Add roster tests

This commit is contained in:
Evgeniy Khramtsov 2016-11-07 10:10:57 +03:00
parent 9d977e484a
commit 56c91d3c58
5 changed files with 727 additions and 360 deletions

View File

@ -442,135 +442,96 @@ online(Sessions) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec do_route(jid(), jid(), stanza() | broadcast()) -> any().
do_route(From, #jid{lresource = <<"">>} = To, {broadcast, _} = Packet) ->
?DEBUG("processing broadcast to bare JID: ~p", [Packet]),
lists:foreach(
fun(R) ->
do_route(From, jid:replace_resource(To, R), Packet)
end, get_user_resources(To#jid.user, To#jid.server));
do_route(From, To, {broadcast, _} = Packet) ->
case To#jid.lresource of
<<"">> ->
lists:foreach(fun(R) ->
do_route(From,
jid:replace_resource(To, R),
Packet)
end,
get_user_resources(To#jid.user, To#jid.server));
_ ->
{U, S, R} = jid:tolower(To),
Mod = get_sm_backend(S),
case online(Mod:get_sessions(U, S, R)) of
[] ->
?DEBUG("packet dropped~n", []);
Ss ->
Session = lists:max(Ss),
Pid = element(2, Session#session.sid),
?DEBUG("sending to process ~p~n", [Pid]),
Pid ! {route, From, To, Packet}
end
?DEBUG("processing broadcast to full JID: ~p", [Packet]),
{U, S, R} = jid:tolower(To),
Mod = get_sm_backend(S),
case online(Mod:get_sessions(U, S, R)) of
[] ->
?DEBUG("dropping broadcast to unavailable resourse: ~p", [Packet]);
Ss ->
Session = lists:max(Ss),
Pid = element(2, Session#session.sid),
?DEBUG("sending to process ~p: ~p", [Pid, Packet]),
Pid ! {route, From, To, Packet}
end;
do_route(From, To, Packet) ->
?DEBUG("session manager~n\tfrom ~p~n\tto ~p~n\tpacket "
"~P~n",
[From, To, Packet, 8]),
do_route(From, To, #presence{type = T, status = Status} = Packet)
when T == subscribe; T == subscribed; T == unsubscribe; T == unsubscribed ->
?DEBUG("processing subscription:~n~s", [xmpp:pp(Packet)]),
#jid{user = User, server = Server,
luser = LUser, lserver = LServer, lresource = LResource} = To,
Lang = xmpp:get_lang(Packet),
case LResource of
<<"">> ->
case Packet of
#presence{type = T, status = Status} ->
{Pass, _Subsc} = case T of
subscribe ->
Reason = xmpp:get_text(Status),
{is_privacy_allow(From, To, Packet)
andalso
ejabberd_hooks:run_fold(roster_in_subscription,
LServer,
false,
[User, Server,
From,
subscribe,
Reason]),
true};
subscribed ->
{is_privacy_allow(From, To, Packet)
andalso
ejabberd_hooks:run_fold(roster_in_subscription,
LServer,
false,
[User, Server,
From,
subscribed,
<<"">>]),
true};
unsubscribe ->
{is_privacy_allow(From, To, Packet)
andalso
ejabberd_hooks:run_fold(roster_in_subscription,
LServer,
false,
[User, Server,
From,
unsubscribe,
<<"">>]),
true};
unsubscribed ->
{is_privacy_allow(From, To, Packet)
andalso
ejabberd_hooks:run_fold(roster_in_subscription,
LServer,
false,
[User, Server,
From,
unsubscribed,
<<"">>]),
true};
_ -> {true, false}
end,
if Pass ->
PResources = get_user_present_resources(LUser, LServer),
lists:foreach(fun ({_, R}) ->
do_route(From,
jid:replace_resource(To,
R),
Packet)
end,
PResources);
true -> ok
end;
#message{type = T} when T == chat; T == headline; T == normal ->
route_message(From, To, Packet, T);
#message{type = groupchat} ->
ErrTxt = <<"User session not found">>,
Err = xmpp:make_error(
Packet, xmpp:err_service_unavailable(ErrTxt, Lang)),
ejabberd_router:route(To, From, Err);
#iq{} -> process_iq(From, To, Packet);
_ -> ok
end;
_ ->
Mod = get_sm_backend(LServer),
case online(Mod:get_sessions(LUser, LServer, LResource)) of
[] ->
case Packet of
#message{type = T} when T == chat; T == normal ->
route_message(From, To, Packet, T);
#message{type = groupchat} ->
ErrTxt = <<"User session not found">>,
Err = xmpp:make_error(
Packet,
xmpp:err_service_unavailable(ErrTxt, Lang)),
ejabberd_router:route(To, From, Err);
#iq{type = T} when T == get; T == set ->
ErrTxt = <<"User session not found">>,
Err = xmpp:make_error(
Packet,
xmpp:err_service_unavailable(ErrTxt, Lang)),
ejabberd_router:route(To, From, Err);
_ -> ?DEBUG("packet dropped~n", [])
end;
Ss ->
Session = lists:max(Ss),
Pid = element(2, Session#session.sid),
?DEBUG("sending to process ~p~n", [Pid]),
Pid ! {route, From, To, Packet}
end
luser = LUser, lserver = LServer} = To,
Reason = if T == subscribe -> xmpp:get_text(Status);
true -> <<"">>
end,
case is_privacy_allow(From, To, Packet) andalso
ejabberd_hooks:run_fold(
roster_in_subscription,
LServer, false,
[User, Server, From, T, Reason]) of
true ->
Mod = get_sm_backend(LServer),
lists:foreach(
fun(#session{sid = SID, usr = {_, _, R},
priority = Prio}) when is_integer(Prio) ->
Pid = element(2, SID),
?DEBUG("sending to process ~p:~n~s",
[Pid, xmpp:pp(Packet)]),
Pid ! {route, From, jid:replace_resource(To, R), Packet};
(_) ->
ok
end, online(Mod:get_sessions(LUser, LServer)));
false ->
ok
end;
do_route(From, #jid{lresource = <<"">>} = To, #presence{} = Packet) ->
?DEBUG("processing presence to bare JID:~n~s", [xmpp:pp(Packet)]),
{LUser, LServer, _} = jid:tolower(To),
lists:foreach(
fun({_, R}) ->
do_route(From, jid:replace_resource(To, R), Packet)
end, get_user_present_resources(LUser, LServer));
do_route(From, #jid{lresource = <<"">>} = To, #message{type = T} = Packet) ->
?DEBUG("processing message to bare JID:~n~s", [xmpp:pp(Packet)]),
if T == chat; T == headline; T == normal ->
route_message(From, To, Packet, T);
true ->
Lang = xmpp:get_lang(Packet),
ErrTxt = <<"User session not found">>,
Err = xmpp:err_service_unavailable(ErrTxt, Lang),
ejabberd_router:route_error(To, From, Packet, Err)
end;
do_route(From, #jid{lresource = <<"">>} = To, #iq{} = Packet) ->
?DEBUG("processing IQ to bare JID:~n~s", [xmpp:pp(Packet)]),
process_iq(From, To, Packet);
do_route(From, To, Packet) ->
?DEBUG("processing packet to full JID:~n~s", [xmpp:pp(Packet)]),
{LUser, LServer, LResource} = jid:tolower(To),
Mod = get_sm_backend(LServer),
case online(Mod:get_sessions(LUser, LServer, LResource)) of
[] ->
case Packet of
#message{type = T} when T == chat; T == normal ->
route_message(From, To, Packet, T);
#presence{} ->
?DEBUG("dropping presence to unavalable resource:~n~s",
[xmpp:pp(Packet)]);
_ ->
Lang = xmpp:get_lang(Packet),
ErrTxt = <<"User session not found">>,
Err = xmpp:err_service_unavailable(ErrTxt, Lang),
ejabberd_router:route_error(To, From, Packet, Err)
end;
Ss ->
Session = lists:max(Ss),
Pid = element(2, Session#session.sid),
?DEBUG("sending to process ~p:~n~s", [Pid, xmpp:pp(Packet)]),
Pid ! {route, From, To, Packet}
end.
%% The default list applies to the user as a whole,

View File

@ -142,21 +142,54 @@ depends(_Host, _Opts) ->
process_iq(#iq{from = #jid{luser = <<"">>},
to = #jid{resource = <<"">>}} = IQ) ->
process_iq_manager(IQ);
process_iq(#iq{from = #jid{luser = U, lserver = S},
to = #jid{luser = U, lserver = S}} = IQ) ->
process_local_iq(IQ);
process_iq(#iq{lang = Lang} = IQ) ->
Txt = <<"Query to another users is forbidden">>,
xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)).
process_iq(#iq{from = From, lang = Lang} = IQ) ->
#jid{lserver = LServer} = From,
case lists:member(LServer, ?MYHOSTS) of
true -> process_local_iq(IQ);
_ ->
Txt = <<"The query is only allowed from local users">>,
xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang))
end.
process_local_iq(#iq{type = Type} = IQ) ->
case Type of
set -> try_process_iq_set(IQ);
get -> process_iq_get(IQ)
end.
process_local_iq(#iq{type = set,lang = Lang,
sub_els = [#roster_query{
items = [#roster_item{ask = Ask}]}]} = IQ)
when Ask /= undefined ->
Txt = <<"Possessing 'ask' attribute is not allowed by RFC6121">>,
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
process_local_iq(#iq{type = set, from = From, lang = Lang,
sub_els = [#roster_query{
items = [#roster_item{} = Item]}]} = IQ) ->
case has_duplicated_groups(Item#roster_item.groups) of
true ->
Txt = <<"Duplicated groups are not allowed by RFC6121">>,
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
false ->
#jid{server = Server} = From,
Access = gen_mod:get_module_opt(Server, ?MODULE,
access, fun(A) -> A end, all),
case acl:match_rule(Server, Access, From) of
deny ->
Txt = <<"Denied by ACL">>,
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
allow ->
process_iq_set(IQ)
end
end;
process_local_iq(#iq{type = set, lang = Lang,
sub_els = [#roster_query{items = [_|_]}]} = IQ) ->
Txt = <<"Multiple <item/> elements are not allowed by RFC6121">>,
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
process_local_iq(#iq{type = get, lang = Lang,
sub_els = [#roster_query{items = Items}]} = IQ) ->
case Items of
[] ->
process_iq_get(IQ);
[_|_] ->
Txt = <<"The query must not contain <item/> elements">>,
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang))
end;
process_local_iq(#iq{lang = Lang} = IQ) ->
Txt = <<"No module is handling this query">>,
xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
roster_hash(Items) ->
p1_sha:sha(term_to_binary(lists:sort([R#roster{groups =
@ -315,11 +348,18 @@ encode_item(Item) ->
end,
groups = Item#roster.groups}.
decode_item(#roster_item{subscription = remove} = Item, R, _) ->
R#roster{jid = jid:tolower(Item#roster_item.jid),
name = <<"">>,
subscription = remove,
ask = none,
groups = [],
askmessage = <<"">>,
xs = []};
decode_item(Item, R, Managed) ->
R#roster{jid = jid:tolower(Item#roster_item.jid),
name = Item#roster_item.name,
subscription = case Item#roster_item.subscription of
remove -> remove;
Sub when Managed -> Sub;
_ -> R#roster.subscription
end,
@ -329,17 +369,6 @@ get_roster_by_jid_t(LUser, LServer, LJID) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:get_roster_by_jid(LUser, LServer, LJID).
try_process_iq_set(#iq{from = From, lang = Lang} = IQ) ->
#jid{server = Server} = From,
Access = gen_mod:get_module_opt(Server, ?MODULE, access, fun(A) -> A end, all),
case acl:match_rule(Server, Access, From) of
deny ->
Txt = <<"Denied by ACL">>,
xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
allow ->
process_iq_set(IQ)
end.
process_iq_set(#iq{from = From, to = To, id = Id,
sub_els = [#roster_query{items = QueryItems}]} = IQ) ->
Managed = is_managed_from_id(Id),
@ -515,8 +544,7 @@ process_subscription(Direction, User, Server, JID1,
{Subscription, Pending} ->
NewItem = Item#roster{subscription = Subscription,
ask = Pending,
askmessage =
iolist_to_binary(AskMessage)},
askmessage = AskMessage},
roster_subscribe_t(LUser, LServer, LJID, NewItem),
case roster_version_on_db(LServer) of
true -> write_roster_version_t(LUser, LServer);
@ -730,10 +758,8 @@ del_roster_t(LUser, LServer, LJID) ->
Mod:del_roster(LUser, LServer, LJID).
process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) ->
JID = {JID1#jid.user, JID1#jid.server,
JID1#jid.resource},
LJID = {JID1#jid.luser, JID1#jid.lserver,
JID1#jid.lresource},
JID = {JID1#jid.user, JID1#jid.server, <<>>},
LJID = {JID1#jid.luser, JID1#jid.lserver, <<>>},
Item = #roster{usj = {LUser, LServer, LJID},
us = {LUser, LServer}, jid = JID},
Item2 = decode_item(QueryItem, Item, _Managed = true),
@ -1046,6 +1072,10 @@ is_managed_from_id(<<"roster-remotely-managed">>) ->
is_managed_from_id(_Id) ->
false.
has_duplicated_groups(Groups) ->
GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]),
not (length(GroupsPrep) == length(Groups)).
export(LServer) ->
Mod = gen_mod:db_mod(LServer, ?MODULE),
Mod:export(LServer).

View File

@ -394,7 +394,7 @@ db_tests(riak) ->
auth_md5,
presence_broadcast,
last,
roster_get,
roster_tests:single_cases(),
private,
privacy_tests:single_cases(),
vcard,
@ -402,9 +402,7 @@ db_tests(riak) ->
test_unregister]},
muc_tests:master_slave_cases(),
privacy_tests:master_slave_cases(),
{test_roster_subscribe, [parallel],
[roster_subscribe_master,
roster_subscribe_slave]},
roster_tests:master_slave_cases(),
{test_flex_offline, [sequence],
[flex_offline_master, flex_offline_slave]},
{test_offline, [sequence],
@ -412,10 +410,7 @@ db_tests(riak) ->
{test_announce, [sequence],
[announce_master, announce_slave]},
{test_vcard_xupdate, [parallel],
[vcard_xupdate_master, vcard_xupdate_slave]},
{test_roster_remove, [parallel],
[roster_remove_master,
roster_remove_slave]}];
[vcard_xupdate_master, vcard_xupdate_slave]}];
db_tests(DB) when DB == mnesia; DB == redis ->
[{single_user, [sequence],
[test_register,
@ -424,8 +419,7 @@ db_tests(DB) when DB == mnesia; DB == redis ->
auth_md5,
presence_broadcast,
last,
roster_get,
roster_ver,
roster_tests:single_cases(),
private,
privacy_tests:single_cases(),
vcard,
@ -435,11 +429,9 @@ db_tests(DB) when DB == mnesia; DB == redis ->
muc_tests:master_slave_cases(),
privacy_tests:master_slave_cases(),
pubsub_multiple_tests(),
roster_tests:master_slave_cases(),
{test_mix, [parallel],
[mix_master, mix_slave]},
{test_roster_subscribe, [parallel],
[roster_subscribe_master,
roster_subscribe_slave]},
{test_flex_offline, [sequence],
[flex_offline_master, flex_offline_slave]},
{test_offline, [sequence],
@ -457,10 +449,7 @@ db_tests(DB) when DB == mnesia; DB == redis ->
{test_announce, [sequence],
[announce_master, announce_slave]},
{test_vcard_xupdate, [parallel],
[vcard_xupdate_master, vcard_xupdate_slave]},
{test_roster_remove, [parallel],
[roster_remove_master,
roster_remove_slave]}];
[vcard_xupdate_master, vcard_xupdate_slave]}];
db_tests(_) ->
%% No support for carboncopy
[{single_user, [sequence],
@ -470,8 +459,7 @@ db_tests(_) ->
auth_md5,
presence_broadcast,
last,
roster_get,
roster_ver,
roster_tests:single_cases(),
private,
privacy_tests:single_cases(),
vcard,
@ -481,11 +469,9 @@ db_tests(_) ->
muc_tests:master_slave_cases(),
privacy_tests:master_slave_cases(),
pubsub_multiple_tests(),
roster_tests:master_slave_cases(),
{test_mix, [parallel],
[mix_master, mix_slave]},
{test_roster_subscribe, [parallel],
[roster_subscribe_master,
roster_subscribe_slave]},
{test_flex_offline, [sequence],
[flex_offline_master, flex_offline_slave]},
{test_offline, [sequence],
@ -499,10 +485,7 @@ db_tests(_) ->
{test_announce, [sequence],
[announce_master, announce_slave]},
{test_vcard_xupdate, [parallel],
[vcard_xupdate_master, vcard_xupdate_slave]},
{test_roster_remove, [parallel],
[roster_remove_master,
roster_remove_slave]}].
[vcard_xupdate_master, vcard_xupdate_slave]}].
ldap_tests() ->
[{ldap_tests, [sequence],
@ -862,33 +845,26 @@ test_bind(Config) ->
test_open_session(Config) ->
disconnect(open_session(Config, true)).
roster_get(Config) ->
#iq{type = result, sub_els = [#roster_query{items = []}]} =
send_recv(Config, #iq{type = get, sub_els = [#roster_query{}]}),
disconnect(Config).
roster_ver(Config) ->
%% Get initial "ver"
#iq{type = result, sub_els = [#roster_query{ver = Ver1, items = []}]} =
send_recv(Config, #iq{type = get,
sub_els = [#roster_query{ver = <<"">>}]}),
%% Should receive empty IQ-result
#iq{type = result, sub_els = []} =
send_recv(Config, #iq{type = get,
sub_els = [#roster_query{ver = Ver1}]}),
%% Attempting to subscribe to server's JID
send(Config, #presence{type = subscribe, to = server_jid(Config)}),
%% Receive a single roster push with the new "ver"
#iq{type = set, sub_els = [#roster_query{ver = Ver2}]} = recv_iq(Config),
%% Requesting roster with the previous "ver". Should receive Ver2 again
#iq{type = result, sub_els = [#roster_query{ver = Ver2}]} =
send_recv(Config, #iq{type = get,
sub_els = [#roster_query{ver = Ver1}]}),
%% Now requesting roster with the newest "ver". Should receive empty IQ.
#iq{type = result, sub_els = []} =
send_recv(Config, #iq{type = get,
sub_els = [#roster_query{ver = Ver2}]}),
disconnect(Config).
roster_feature_enabled(Config) ->
roster_tests:feature_enabled(Config).
roster_iq_set_many_items(Config) ->
roster_tests:iq_set_many_items(Config).
roster_iq_set_duplicated_groups(Config) ->
roster_tests:iq_set_duplicated_groups(Config).
roster_iq_set_ask(Config) ->
roster_tests:iq_set_ask(Config).
roster_iq_get_item(Config) ->
roster_tests:iq_get_item(Config).
roster_iq_unexpected_element(Config) ->
roster_tests:iq_unexpected_element(Config).
roster_set_item(Config) ->
roster_tests:set_item(Config).
roster_version(Config) ->
roster_tests:version(Config).
roster_subscribe_master(Config) ->
roster_tests:subscribe_master(Config).
roster_subscribe_slave(Config) ->
roster_tests:subscribe_slave(Config).
codec_failure(Config) ->
JID = my_jid(Config),
@ -2043,148 +2019,6 @@ mix_slave(Config) ->
disconnect = get_event(Config),
disconnect(Config).
roster_subscribe_master(Config) ->
#presence{} = send_recv(Config, #presence{}),
wait_for_slave(Config),
Peer = ?config(peer, Config),
LPeer = jid:remove_resource(Peer),
send(Config, #presence{type = subscribe, to = LPeer}),
Push1 = #iq{type = set,
sub_els = [#roster_query{items = [#roster_item{
ask = subscribe,
subscription = none,
jid = LPeer}]}]} =
recv_iq(Config),
send(Config, make_iq_result(Push1)),
#presence{type = subscribed, from = LPeer} = recv_presence(Config),
Push2 = #iq{type = set,
sub_els = [#roster_query{items = [#roster_item{
subscription = to,
jid = LPeer}]}]} =
recv_iq(Config),
send(Config, make_iq_result(Push2)),
#presence{type = available, from = Peer} = recv_presence(Config),
%% BUG: ejabberd sends previous push again. Is it ok?
Push3 = #iq{type = set,
sub_els = [#roster_query{items = [#roster_item{
subscription = to,
jid = LPeer}]}]} =
recv_iq(Config),
send(Config, make_iq_result(Push3)),
#presence{type = subscribe, from = LPeer} = recv_presence(Config),
send(Config, #presence{type = subscribed, to = LPeer}),
Push4 = #iq{type = set,
sub_els = [#roster_query{items = [#roster_item{
subscription = both,
jid = LPeer}]}]} =
recv_iq(Config),
send(Config, make_iq_result(Push4)),
%% Move into a group
Groups = [<<"A">>, <<"B">>],
Item = #roster_item{jid = LPeer, groups = Groups},
#iq{type = result, sub_els = []} =
send_recv(Config,
#iq{type = set, sub_els = [#roster_query{items = [Item]}]}),
Push5 = #iq{type = set,
sub_els =
[#roster_query{items = [#roster_item{
jid = LPeer,
subscription = both}]}]} =
recv_iq(Config),
send(Config, make_iq_result(Push5)),
#iq{sub_els = [#roster_query{items = [#roster_item{groups = G1}]}]} = Push5,
Groups = lists:sort(G1),
wait_for_slave(Config),
#presence{type = unavailable, from = Peer} = recv_presence(Config),
disconnect(Config).
roster_subscribe_slave(Config) ->
#presence{} = send_recv(Config, #presence{}),
wait_for_master(Config),
Peer = ?config(master, Config),
LPeer = jid:remove_resource(Peer),
#presence{type = subscribe, from = LPeer} = recv_presence(Config),
send(Config, #presence{type = subscribed, to = LPeer}),
Push1 = #iq{type = set,
sub_els = [#roster_query{items = [#roster_item{
subscription = from,
jid = LPeer}]}]} =
recv_iq(Config),
send(Config, make_iq_result(Push1)),
send(Config, #presence{type = subscribe, to = LPeer}),
Push2 = #iq{type = set,
sub_els = [#roster_query{items = [#roster_item{
ask = subscribe,
subscription = from,
jid = LPeer}]}]} =
recv_iq(Config),
send(Config, make_iq_result(Push2)),
#presence{type = subscribed, from = LPeer} = recv_presence(Config),
Push3 = #iq{type = set,
sub_els = [#roster_query{items = [#roster_item{
subscription = both,
jid = LPeer}]}]} =
recv_iq(Config),
send(Config, make_iq_result(Push3)),
#presence{type = available, from = Peer} = recv_presence(Config),
wait_for_master(Config),
disconnect(Config).
roster_remove_master(Config) ->
MyJID = my_jid(Config),
Peer = ?config(slave, Config),
LPeer = jid:remove_resource(Peer),
Groups = [<<"A">>, <<"B">>],
wait_for_slave(Config),
#presence{from = MyJID, type = available} = send_recv(Config, #presence{}),
#presence{from = Peer, type = available} = recv_presence(Config),
%% The peer removed us from its roster.
{Push1, Push2, _, _, _} =
?recv5(
%% TODO: I guess this can be optimized, we don't need
%% to send transient roster push with subscription = 'to'.
#iq{type = set,
sub_els =
[#roster_query{items = [#roster_item{
jid = LPeer,
subscription = to}]}]},
#iq{type = set,
sub_els =
[#roster_query{items = [#roster_item{
jid = LPeer,
subscription = none}]}]},
#presence{type = unsubscribe, from = LPeer},
#presence{type = unsubscribed, from = LPeer},
#presence{type = unavailable, from = Peer}),
send(Config, make_iq_result(Push1)),
send(Config, make_iq_result(Push2)),
#iq{sub_els = [#roster_query{items = [#roster_item{groups = G1}]}]} = Push1,
#iq{sub_els = [#roster_query{items = [#roster_item{groups = G2}]}]} = Push2,
Groups = lists:sort(G1), Groups = lists:sort(G2),
disconnect(Config).
roster_remove_slave(Config) ->
MyJID = my_jid(Config),
Peer = ?config(master, Config),
LPeer = jid:remove_resource(Peer),
#presence{from = MyJID, type = available} = send_recv(Config, #presence{}),
wait_for_master(Config),
#presence{from = Peer, type = available} = recv_presence(Config),
%% Remove the peer from roster.
Item = #roster_item{jid = LPeer, subscription = remove},
#iq{type = result, sub_els = []} =
send_recv(Config, #iq{type = set,
sub_els = [#roster_query{items = [Item]}]}),
Push = #iq{type = set,
sub_els =
[#roster_query{items = [#roster_item{
jid = LPeer,
subscription = remove}]}]} =
recv_iq(Config),
#presence{type = unavailable, from = Peer} = recv_presence(Config),
send(Config, make_iq_result(Push)),
disconnect(Config).
proxy65_master(Config) ->
Proxy = proxy_jid(Config),
MyJID = my_jid(Config),

527
test/roster_tests.erl Normal file
View File

@ -0,0 +1,527 @@
%%%-------------------------------------------------------------------
%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
%%% @copyright (C) 2016, Evgeny Khramtsov
%%% @doc
%%%
%%% @end
%%% Created : 22 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
%%%-------------------------------------------------------------------
-module(roster_tests).
%% API
-compile(export_all).
-import(suite, [send_recv/2, recv_iq/1, send/2, disconnect/1, del_roster/1,
del_roster/2, make_iq_result/1, wait_for_slave/1,
wait_for_master/1, recv_presence/1, self_presence/2,
put_event/2, get_event/1, match_failure/2, get_roster/1,
is_feature_advertised/2]).
-include("suite.hrl").
-include("mod_roster.hrl").
-record(state, {subscription = none :: none | from | to | both,
peer_available = false,
pending_in = false :: boolean(),
pending_out = false :: boolean()}).
%%%===================================================================
%%% API
%%%===================================================================
init(_TestCase, Config) ->
Config.
stop(_TestCase, Config) ->
Config.
%%%===================================================================
%%% Single user tests
%%%===================================================================
single_cases() ->
{roster_single, [sequence],
[single_test(feature_enabled),
single_test(iq_set_many_items),
single_test(iq_set_duplicated_groups),
single_test(iq_get_item),
single_test(iq_unexpected_element),
single_test(iq_set_ask),
single_test(set_item),
single_test(version)]}.
feature_enabled(Config) ->
ct:comment("Checking if roster versioning stream feature is set"),
true = ?config(rosterver, Config),
disconnect(Config).
set_item(Config) ->
JID = jid:from_string(<<"nurse@example.com">>),
Item = #roster_item{jid = JID},
{V1, Item} = set_items(Config, [Item]),
{V1, [Item]} = get_items(Config),
ItemWithGroups = Item#roster_item{groups = [<<"G1">>, <<"G2">>]},
{V2, ItemWithGroups} = set_items(Config, [ItemWithGroups]),
{V2, [ItemWithGroups]} = get_items(Config),
{V3, Item} = set_items(Config, [Item]),
{V3, [Item]} = get_items(Config),
ItemWithName = Item#roster_item{name = <<"some name">>},
{V4, ItemWithName} = set_items(Config, [ItemWithName]),
{V4, [ItemWithName]} = get_items(Config),
ItemRemoved = Item#roster_item{subscription = remove},
{V5, ItemRemoved} = set_items(Config, [ItemRemoved]),
{V5, []} = get_items(Config),
del_roster(disconnect(Config), JID).
iq_set_many_items(Config) ->
J1 = jid:from_string(<<"nurse1@example.com">>),
J2 = jid:from_string(<<"nurse2@example.com">>),
ct:comment("Trying to send roster-set with many <item/> elements"),
Items = [#roster_item{jid = J1}, #roster_item{jid = J2}],
#stanza_error{reason = 'bad-request'} = set_items(Config, Items),
disconnect(Config).
iq_set_duplicated_groups(Config) ->
JID = jid:from_string(<<"nurse@example.com">>),
G = randoms:get_string(),
ct:comment("Trying to send roster-set with duplicated groups"),
Item = #roster_item{jid = JID, groups = [G, G]},
#stanza_error{reason = 'bad-request'} = set_items(Config, [Item]),
disconnect(Config).
iq_set_ask(Config) ->
JID = jid:from_string(<<"nurse@example.com">>),
ct:comment("Trying to send roster-set with 'ask' included"),
Item = #roster_item{jid = JID, ask = subscribe},
#stanza_error{reason = 'bad-request'} = set_items(Config, [Item]),
disconnect(Config).
iq_get_item(Config) ->
JID = jid:from_string(<<"nurse@example.com">>),
ct:comment("Trying to send roster-get with <item/> element"),
#iq{type = error} = Err3 =
send_recv(Config, #iq{type = get,
sub_els = [#roster_query{
items = [#roster_item{jid = JID}]}]}),
#stanza_error{reason = 'bad-request'} = xmpp:get_error(Err3),
disconnect(Config).
iq_unexpected_element(Config) ->
JID = jid:from_string(<<"nurse@example.com">>),
ct:comment("Trying to send IQs with unexpected element"),
lists:foreach(
fun(Type) ->
#iq{type = error} = Err4 =
send_recv(Config, #iq{type = Type,
sub_els = [#roster_item{jid = JID}]}),
#stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err4)
end, [get, set]),
disconnect(Config).
version(Config) ->
JID = jid:from_string(<<"nurse@example.com">>),
ct:comment("Requesting roster"),
{InitialVersion, _} = get_items(Config, <<"">>),
ct:comment("Requesting roster with initial version"),
{empty, []} = get_items(Config, InitialVersion),
ct:comment("Adding JID to the roster"),
{NewVersion, _} = set_items(Config, [#roster_item{jid = JID}]),
ct:comment("Requesting roster with initial version"),
{NewVersion, _} = get_items(Config, InitialVersion),
ct:comment("Requesting roster with new version"),
{empty, []} = get_items(Config, NewVersion),
del_roster(disconnect(Config), JID).
%%%===================================================================
%%% Master-slave tests
%%%===================================================================
master_slave_cases() ->
{roster_master_slave, [parallel],
[master_slave_test(subscribe)]}.
subscribe_master(Config) ->
Actions = actions(),
process_subscriptions_master(Config, Actions),
del_roster(disconnect(Config)).
subscribe_slave(Config) ->
process_subscriptions_slave(Config),
del_roster(disconnect(Config)).
process_subscriptions_master(Config, Actions) ->
EnumeratedActions = lists:zip(lists:seq(1, length(Actions)), Actions),
self_presence(Config, available),
lists:foldl(
fun({N, {Dir, Type}}, State) ->
if Dir == out -> put_event(Config, {N, in, Type});
Dir == in -> put_event(Config, {N, out, Type})
end,
wait_for_slave(Config),
ct:pal("Performing ~s-~s (#~p) "
"in state:~n~s~nwith roster:~n~s",
[Dir, Type, N, pp(State),
pp(get_roster(Config))]),
transition(Config, Dir, Type, State)
end, #state{}, EnumeratedActions),
put_event(Config, done),
wait_for_slave(Config),
Config.
process_subscriptions_slave(Config) ->
self_presence(Config, available),
process_subscriptions_slave(Config, get_event(Config), #state{}).
process_subscriptions_slave(Config, done, _State) ->
wait_for_master(Config),
Config;
process_subscriptions_slave(Config, {N, Dir, Type}, State) ->
wait_for_master(Config),
ct:pal("Performing ~s-~s (#~p) "
"in state:~n~s~nwith roster:~n~s",
[Dir, Type, N, pp(State), pp(get_roster(Config))]),
NewState = transition(Config, Dir, Type, State),
process_subscriptions_slave(Config, get_event(Config), NewState).
%%%===================================================================
%%% Internal functions
%%%===================================================================
single_test(T) ->
list_to_atom("roster_" ++ atom_to_list(T)).
master_slave_test(T) ->
{list_to_atom("roster_" ++ atom_to_list(T)), [parallel],
[list_to_atom("roster_" ++ atom_to_list(T) ++ "_master"),
list_to_atom("roster_" ++ atom_to_list(T) ++ "_slave")]}.
get_items(Config) ->
get_items(Config, <<"">>).
get_items(Config, Version) ->
case send_recv(Config, #iq{type = get,
sub_els = [#roster_query{ver = Version}]}) of
#iq{type = result,
sub_els = [#roster_query{ver = NewVersion, items = Items}]} ->
{NewVersion, Items};
#iq{type = result, sub_els = []} ->
{empty, []};
#iq{type = error} = Err ->
xmpp:get_error(Err)
end.
get_item(Config, JID) ->
case get_items(Config) of
{_Ver, Items} when is_list(Items) ->
lists:keyfind(JID, #roster_item.jid, Items);
_ ->
false
end.
set_items(Config, Items) ->
case send_recv(Config, #iq{type = set,
sub_els = [#roster_query{items = Items}]}) of
#iq{type = result, sub_els = []} ->
recv_push(Config);
#iq{type = error} = Err ->
xmpp:get_error(Err)
end.
recv_push(Config) ->
ct:comment("Receiving roster push"),
Push = #iq{type = set,
sub_els = [#roster_query{ver = Ver, items = [PushItem]}]}
= recv_iq(Config),
send(Config, make_iq_result(Push)),
{Ver, PushItem}.
recv_push(Config, Subscription, Ask) ->
PeerJID = ?config(peer, Config),
PeerBareJID = jid:remove_resource(PeerJID),
Match = #roster_item{jid = PeerBareJID,
subscription = Subscription,
ask = Ask,
groups = [],
name = <<"">>},
ct:comment("Receiving roster push"),
Push = #iq{type = set, sub_els = [#roster_query{items = [Item]}]} =
recv_iq(Config),
case Item of
Match -> send(Config, make_iq_result(Push));
_ -> match_failure(Item, Match)
end.
recv_presence(Config, Type) ->
PeerJID = ?config(peer, Config),
case recv_presence(Config) of
#presence{from = PeerJID, type = Type} -> ok;
Pres -> match_failure(Pres, #presence{from = PeerJID, type = Type})
end.
recv_subscription(Config, Type) ->
PeerJID = ?config(peer, Config),
PeerBareJID = jid:remove_resource(PeerJID),
case recv_presence(Config) of
#presence{from = PeerBareJID, type = Type} -> ok;
Pres -> match_failure(Pres, #presence{from = PeerBareJID, type = Type})
end.
pp(Term) ->
io_lib_pretty:print(Term, fun pp/2).
pp(state, N) ->
Fs = record_info(fields, state),
try N = length(Fs), Fs
catch _:_ -> no end;
pp(roster, N) ->
Fs = record_info(fields, roster),
try N = length(Fs), Fs
catch _:_ -> no end;
pp(_, _) -> no.
%% RFC6121, A.2.1
transition(Config, out, subscribe,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
PeerJID = ?config(peer, Config),
PeerBareJID = jid:remove_resource(PeerJID),
send(Config, #presence{to = PeerBareJID, type = subscribe}),
case {Sub, Out, In} of
{none, false, _} ->
recv_push(Config, none, subscribe),
State#state{pending_out = true};
{none, true, false} ->
%% BUG: we should not receive roster push here
recv_push(Config, none, subscribe),
State;
{from, false, false} ->
recv_push(Config, from, subscribe),
State#state{pending_out = true};
_ ->
State
end;
%% RFC6121, A.2.2
transition(Config, out, unsubscribe,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
PeerJID = ?config(peer, Config),
PeerBareJID = jid:remove_resource(PeerJID),
send(Config, #presence{to = PeerBareJID, type = unsubscribe}),
case {Sub, Out, In} of
{none, true, _} ->
recv_push(Config, none, undefined),
State#state{pending_out = false};
{to, false, _} ->
recv_push(Config, none, undefined),
recv_presence(Config, unavailable),
State#state{subscription = none, peer_available = false};
{from, true, false} ->
recv_push(Config, from, undefined),
State#state{pending_out = false};
{both, false, false} ->
recv_push(Config, from, undefined),
recv_presence(Config, unavailable),
State#state{subscription = from, peer_available = false};
_ ->
State
end;
%% RFC6121, A.2.3
transition(Config, out, subscribed,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
PeerJID = ?config(peer, Config),
PeerBareJID = jid:remove_resource(PeerJID),
send(Config, #presence{to = PeerBareJID, type = subscribed}),
case {Sub, Out, In} of
{none, false, true} ->
recv_push(Config, from, undefined),
State#state{subscription = from, pending_in = false};
{none, true, true} ->
recv_push(Config, from, subscribe),
State#state{subscription = from, pending_in = false};
{to, false, true} ->
recv_push(Config, both, undefined),
State#state{subscription = both, pending_in = false};
{to, false, _} ->
%% BUG: we should not transition to 'both' state
recv_push(Config, both, undefined),
State#state{subscription = both};
_ ->
State
end;
%% RFC6121, A.2.4
transition(Config, out, unsubscribed,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
PeerJID = ?config(peer, Config),
PeerBareJID = jid:remove_resource(PeerJID),
send(Config, #presence{to = PeerBareJID, type = unsubscribed}),
case {Sub, Out, In} of
{none, false, true} ->
State#state{subscription = none, pending_in = false};
{none, true, true} ->
recv_push(Config, none, subscribe),
State#state{subscription = none, pending_in = false};
{to, _, true} ->
State#state{pending_in = false};
{from, false, _} ->
recv_push(Config, none, undefined),
State#state{subscription = none};
{from, true, _} ->
recv_push(Config, none, subscribe),
State#state{subscription = none};
{both, _, _} ->
recv_push(Config, to, undefined),
State#state{subscription = to};
_ ->
State
end;
%% RFC6121, A.3.1
transition(Config, in, subscribe = Type,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
case {Sub, Out, In} of
{none, false, false} ->
recv_subscription(Config, Type),
State#state{pending_in = true};
{none, true, false} ->
recv_push(Config, none, subscribe),
recv_subscription(Config, Type),
State#state{pending_in = true};
{to, false, false} ->
%% BUG: we should not receive roster push in this state!
recv_push(Config, to, undefined),
recv_subscription(Config, Type),
State#state{pending_in = true};
_ ->
State
end;
%% RFC6121, A.3.2
transition(Config, in, unsubscribe = Type,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
case {Sub, Out, In} of
{none, _, true} ->
State#state{pending_in = false};
{to, _, true} ->
recv_push(Config, to, undefined),
recv_subscription(Config, Type),
State#state{pending_in = false};
{from, false, _} ->
recv_push(Config, none, undefined),
recv_subscription(Config, Type),
State#state{subscription = none};
{from, true, _} ->
recv_push(Config, none, subscribe),
recv_subscription(Config, Type),
State#state{subscription = none};
{both, _, _} ->
recv_push(Config, to, undefined),
recv_subscription(Config, Type),
State#state{subscription = to};
_ ->
State
end;
%% RFC6121, A.3.3
transition(Config, in, subscribed = Type,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
case {Sub, Out, In} of
{none, true, _} ->
recv_push(Config, to, undefined),
recv_subscription(Config, Type),
recv_presence(Config, available),
State#state{subscription = to, pending_out = false, peer_available = true};
{from, true, _} ->
recv_push(Config, both, undefined),
recv_subscription(Config, Type),
recv_presence(Config, available),
State#state{subscription = both, pending_out = false, peer_available = true};
{from, false, _} ->
%% BUG: we should not transition to 'both' in this state
recv_push(Config, both, undefined),
recv_subscription(Config, Type),
recv_presence(Config, available),
State#state{subscription = both, pending_out = false, peer_available = true};
_ ->
State
end;
%% RFC6121, A.3.4
transition(Config, in, unsubscribed = Type,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
case {Sub, Out, In} of
{none, true, true} ->
%% BUG: we should receive roster push in this state!
recv_subscription(Config, Type),
State#state{subscription = none, pending_out = false};
{none, true, false} ->
recv_push(Config, none, undefined),
recv_subscription(Config, Type),
State#state{subscription = none, pending_out = false};
{none, false, false} ->
State;
{to, false, _} ->
recv_push(Config, none, undefined),
recv_subscription(Config, Type),
recv_presence(Config, unavailable),
State#state{subscription = none, peer_available = false};
{from, true, false} ->
recv_push(Config, from, undefined),
recv_subscription(Config, Type),
State#state{subscription = from, pending_out = false};
{both, _, _} ->
recv_push(Config, from, undefined),
recv_subscription(Config, Type),
recv_presence(Config, unavailable),
State#state{subscription = from, peer_available = false};
_ ->
State
end;
%% Outgoing roster remove
transition(Config, out, remove,
#state{subscription = Sub, pending_in = In, pending_out = Out}) ->
PeerJID = ?config(peer, Config),
PeerBareJID = jid:remove_resource(PeerJID),
Item = #roster_item{jid = PeerBareJID, subscription = remove},
#iq{type = result, sub_els = []} =
send_recv(Config, #iq{type = set,
sub_els = [#roster_query{items = [Item]}]}),
recv_push(Config, remove, undefined),
case {Sub, Out, In} of
{to, _, _} ->
recv_presence(Config, unavailable);
{both, _, _} ->
recv_presence(Config, unavailable);
_ ->
ok
end,
#state{};
%% Incoming roster remove
transition(Config, in, remove,
#state{subscription = Sub, pending_in = In, pending_out = Out} = State) ->
case {Sub, Out, In} of
{none, true, _} ->
ok;
{from, false, _} ->
recv_push(Config, none, undefined),
recv_subscription(Config, unsubscribe);
{from, true, _} ->
recv_push(Config, none, subscribe),
recv_subscription(Config, unsubscribe);
{to, false, _} ->
%% BUG: we should receive push here
%% recv_push(Config, none, undefined),
recv_presence(Config, unavailable),
recv_subscription(Config, unsubscribed);
{both, _, _} ->
recv_presence(Config, unavailable),
recv_push(Config, to, undefined),
recv_subscription(Config, unsubscribe),
recv_push(Config, none, undefined),
recv_subscription(Config, unsubscribed);
_ ->
ok
end,
State#state{subscription = none}.
actions() ->
States = [{Dir, Type} || Dir <- [out, in],
Type <- [subscribe, subscribed,
unsubscribe, unsubscribed,
remove]],
Actions = lists:flatten([[X, Y] || X <- States, Y <- States]),
remove_dups(Actions, []).
remove_dups([X|T], [X,X|_] = Acc) ->
remove_dups(T, Acc);
remove_dups([X|T], Acc) ->
remove_dups(T, [X|Acc]);
remove_dups([], Acc) ->
lists:reverse(Acc).

View File

@ -86,6 +86,7 @@ init_config(Config) ->
{stream_from, <<"">>},
{db_xmlns, <<"">>},
{mechs, []},
{rosterver, false},
{lang, <<"en">>},
{base_dir, BaseDir},
{socket, undefined},
@ -421,6 +422,8 @@ wait_auth_SASL_result(Config, ShouldFail) ->
set_opt(sm, true, ConfigAcc);
(#feature_csi{}, ConfigAcc) ->
set_opt(csi, true, ConfigAcc);
(#rosterver_feature{}, ConfigAcc) ->
set_opt(rosterver, true, ConfigAcc);
(_, ConfigAcc) ->
ConfigAcc
end, Config, Fs);
@ -674,26 +677,32 @@ set_opt(Opt, Val, Config) ->
[{Opt, Val}|lists:keydelete(Opt, 1, Config)].
wait_for_master(Config) ->
put_event(Config, slave_ready),
put_event(Config, peer_ready),
case get_event(Config) of
master_ready ->
peer_ready ->
ok;
Other ->
suite:match_failure([Other], [master_ready])
suite:match_failure(Other, peer_ready)
end.
wait_for_slave(Config) ->
put_event(Config, master_ready),
put_event(Config, peer_ready),
case get_event(Config) of
slave_ready ->
peer_ready ->
ok;
Other ->
suite:match_failure([Other], [slave_ready])
suite:match_failure(Other, peer_ready)
end.
make_iq_result(#iq{from = From} = IQ) ->
IQ#iq{type = result, to = From, from = undefined, sub_els = []}.
self_presence(Config, Type) ->
MyJID = my_jid(Config),
ct:comment("Sending self-presence"),
#presence{type = Type, from = MyJID} =
send_recv(Config, #presence{type = Type}).
set_roster(Config, Subscription, Groups) ->
MyJID = my_jid(Config),
{U, S, _} = jid:tolower(MyJID),
@ -710,15 +719,21 @@ set_roster(Config, Subscription, Groups) ->
Config.
del_roster(Config) ->
del_roster(Config, ?config(peer, Config)).
del_roster(Config, PeerJID) ->
MyJID = my_jid(Config),
{U, S, _} = jid:tolower(MyJID),
PeerJID = ?config(peer, Config),
PeerBareJID = jid:remove_resource(PeerJID),
PeerLJID = jid:tolower(PeerBareJID),
ct:comment("Removing ~s from roster", [jid:to_string(PeerBareJID)]),
{atomic, _} = mod_roster:del_roster(U, S, PeerLJID),
Config.
get_roster(Config) ->
{LUser, LServer, _} = jid:tolower(my_jid(Config)),
mod_roster:get_roster(LUser, LServer).
receiver(NS, Owner) ->
MRef = erlang:monitor(process, Owner),
receiver(NS, Owner, MRef).