mirror of
https://github.com/processone/ejabberd.git
synced 2024-12-26 17:38:45 +01:00
Add roster tests
This commit is contained in:
parent
9d977e484a
commit
56c91d3c58
@ -442,135 +442,96 @@ online(Sessions) ->
|
|||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
-spec do_route(jid(), jid(), stanza() | broadcast()) -> any().
|
-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) ->
|
do_route(From, To, {broadcast, _} = Packet) ->
|
||||||
case To#jid.lresource of
|
?DEBUG("processing broadcast to full JID: ~p", [Packet]),
|
||||||
<<"">> ->
|
{U, S, R} = jid:tolower(To),
|
||||||
lists:foreach(fun(R) ->
|
Mod = get_sm_backend(S),
|
||||||
do_route(From,
|
case online(Mod:get_sessions(U, S, R)) of
|
||||||
jid:replace_resource(To, R),
|
[] ->
|
||||||
Packet)
|
?DEBUG("dropping broadcast to unavailable resourse: ~p", [Packet]);
|
||||||
end,
|
Ss ->
|
||||||
get_user_resources(To#jid.user, To#jid.server));
|
Session = lists:max(Ss),
|
||||||
_ ->
|
Pid = element(2, Session#session.sid),
|
||||||
{U, S, R} = jid:tolower(To),
|
?DEBUG("sending to process ~p: ~p", [Pid, Packet]),
|
||||||
Mod = get_sm_backend(S),
|
Pid ! {route, From, To, Packet}
|
||||||
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
|
|
||||||
end;
|
end;
|
||||||
do_route(From, To, Packet) ->
|
do_route(From, To, #presence{type = T, status = Status} = Packet)
|
||||||
?DEBUG("session manager~n\tfrom ~p~n\tto ~p~n\tpacket "
|
when T == subscribe; T == subscribed; T == unsubscribe; T == unsubscribed ->
|
||||||
"~P~n",
|
?DEBUG("processing subscription:~n~s", [xmpp:pp(Packet)]),
|
||||||
[From, To, Packet, 8]),
|
|
||||||
#jid{user = User, server = Server,
|
#jid{user = User, server = Server,
|
||||||
luser = LUser, lserver = LServer, lresource = LResource} = To,
|
luser = LUser, lserver = LServer} = To,
|
||||||
Lang = xmpp:get_lang(Packet),
|
Reason = if T == subscribe -> xmpp:get_text(Status);
|
||||||
case LResource of
|
true -> <<"">>
|
||||||
<<"">> ->
|
end,
|
||||||
case Packet of
|
case is_privacy_allow(From, To, Packet) andalso
|
||||||
#presence{type = T, status = Status} ->
|
ejabberd_hooks:run_fold(
|
||||||
{Pass, _Subsc} = case T of
|
roster_in_subscription,
|
||||||
subscribe ->
|
LServer, false,
|
||||||
Reason = xmpp:get_text(Status),
|
[User, Server, From, T, Reason]) of
|
||||||
{is_privacy_allow(From, To, Packet)
|
true ->
|
||||||
andalso
|
Mod = get_sm_backend(LServer),
|
||||||
ejabberd_hooks:run_fold(roster_in_subscription,
|
lists:foreach(
|
||||||
LServer,
|
fun(#session{sid = SID, usr = {_, _, R},
|
||||||
false,
|
priority = Prio}) when is_integer(Prio) ->
|
||||||
[User, Server,
|
Pid = element(2, SID),
|
||||||
From,
|
?DEBUG("sending to process ~p:~n~s",
|
||||||
subscribe,
|
[Pid, xmpp:pp(Packet)]),
|
||||||
Reason]),
|
Pid ! {route, From, jid:replace_resource(To, R), Packet};
|
||||||
true};
|
(_) ->
|
||||||
subscribed ->
|
ok
|
||||||
{is_privacy_allow(From, To, Packet)
|
end, online(Mod:get_sessions(LUser, LServer)));
|
||||||
andalso
|
false ->
|
||||||
ejabberd_hooks:run_fold(roster_in_subscription,
|
ok
|
||||||
LServer,
|
end;
|
||||||
false,
|
do_route(From, #jid{lresource = <<"">>} = To, #presence{} = Packet) ->
|
||||||
[User, Server,
|
?DEBUG("processing presence to bare JID:~n~s", [xmpp:pp(Packet)]),
|
||||||
From,
|
{LUser, LServer, _} = jid:tolower(To),
|
||||||
subscribed,
|
lists:foreach(
|
||||||
<<"">>]),
|
fun({_, R}) ->
|
||||||
true};
|
do_route(From, jid:replace_resource(To, R), Packet)
|
||||||
unsubscribe ->
|
end, get_user_present_resources(LUser, LServer));
|
||||||
{is_privacy_allow(From, To, Packet)
|
do_route(From, #jid{lresource = <<"">>} = To, #message{type = T} = Packet) ->
|
||||||
andalso
|
?DEBUG("processing message to bare JID:~n~s", [xmpp:pp(Packet)]),
|
||||||
ejabberd_hooks:run_fold(roster_in_subscription,
|
if T == chat; T == headline; T == normal ->
|
||||||
LServer,
|
route_message(From, To, Packet, T);
|
||||||
false,
|
true ->
|
||||||
[User, Server,
|
Lang = xmpp:get_lang(Packet),
|
||||||
From,
|
ErrTxt = <<"User session not found">>,
|
||||||
unsubscribe,
|
Err = xmpp:err_service_unavailable(ErrTxt, Lang),
|
||||||
<<"">>]),
|
ejabberd_router:route_error(To, From, Packet, Err)
|
||||||
true};
|
end;
|
||||||
unsubscribed ->
|
do_route(From, #jid{lresource = <<"">>} = To, #iq{} = Packet) ->
|
||||||
{is_privacy_allow(From, To, Packet)
|
?DEBUG("processing IQ to bare JID:~n~s", [xmpp:pp(Packet)]),
|
||||||
andalso
|
process_iq(From, To, Packet);
|
||||||
ejabberd_hooks:run_fold(roster_in_subscription,
|
do_route(From, To, Packet) ->
|
||||||
LServer,
|
?DEBUG("processing packet to full JID:~n~s", [xmpp:pp(Packet)]),
|
||||||
false,
|
{LUser, LServer, LResource} = jid:tolower(To),
|
||||||
[User, Server,
|
Mod = get_sm_backend(LServer),
|
||||||
From,
|
case online(Mod:get_sessions(LUser, LServer, LResource)) of
|
||||||
unsubscribed,
|
[] ->
|
||||||
<<"">>]),
|
case Packet of
|
||||||
true};
|
#message{type = T} when T == chat; T == normal ->
|
||||||
_ -> {true, false}
|
route_message(From, To, Packet, T);
|
||||||
end,
|
#presence{} ->
|
||||||
if Pass ->
|
?DEBUG("dropping presence to unavalable resource:~n~s",
|
||||||
PResources = get_user_present_resources(LUser, LServer),
|
[xmpp:pp(Packet)]);
|
||||||
lists:foreach(fun ({_, R}) ->
|
_ ->
|
||||||
do_route(From,
|
Lang = xmpp:get_lang(Packet),
|
||||||
jid:replace_resource(To,
|
ErrTxt = <<"User session not found">>,
|
||||||
R),
|
Err = xmpp:err_service_unavailable(ErrTxt, Lang),
|
||||||
Packet)
|
ejabberd_router:route_error(To, From, Packet, Err)
|
||||||
end,
|
end;
|
||||||
PResources);
|
Ss ->
|
||||||
true -> ok
|
Session = lists:max(Ss),
|
||||||
end;
|
Pid = element(2, Session#session.sid),
|
||||||
#message{type = T} when T == chat; T == headline; T == normal ->
|
?DEBUG("sending to process ~p:~n~s", [Pid, xmpp:pp(Packet)]),
|
||||||
route_message(From, To, Packet, T);
|
Pid ! {route, From, To, Packet}
|
||||||
#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
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% The default list applies to the user as a whole,
|
%% The default list applies to the user as a whole,
|
||||||
|
@ -142,21 +142,54 @@ depends(_Host, _Opts) ->
|
|||||||
process_iq(#iq{from = #jid{luser = <<"">>},
|
process_iq(#iq{from = #jid{luser = <<"">>},
|
||||||
to = #jid{resource = <<"">>}} = IQ) ->
|
to = #jid{resource = <<"">>}} = IQ) ->
|
||||||
process_iq_manager(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) ->
|
process_local_iq(#iq{type = set,lang = Lang,
|
||||||
#jid{lserver = LServer} = From,
|
sub_els = [#roster_query{
|
||||||
case lists:member(LServer, ?MYHOSTS) of
|
items = [#roster_item{ask = Ask}]}]} = IQ)
|
||||||
true -> process_local_iq(IQ);
|
when Ask /= undefined ->
|
||||||
_ ->
|
Txt = <<"Possessing 'ask' attribute is not allowed by RFC6121">>,
|
||||||
Txt = <<"The query is only allowed from local users">>,
|
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
|
||||||
xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang))
|
process_local_iq(#iq{type = set, from = From, lang = Lang,
|
||||||
end.
|
sub_els = [#roster_query{
|
||||||
|
items = [#roster_item{} = Item]}]} = IQ) ->
|
||||||
process_local_iq(#iq{type = Type} = IQ) ->
|
case has_duplicated_groups(Item#roster_item.groups) of
|
||||||
case Type of
|
true ->
|
||||||
set -> try_process_iq_set(IQ);
|
Txt = <<"Duplicated groups are not allowed by RFC6121">>,
|
||||||
get -> process_iq_get(IQ)
|
xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang));
|
||||||
end.
|
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) ->
|
roster_hash(Items) ->
|
||||||
p1_sha:sha(term_to_binary(lists:sort([R#roster{groups =
|
p1_sha:sha(term_to_binary(lists:sort([R#roster{groups =
|
||||||
@ -315,11 +348,18 @@ encode_item(Item) ->
|
|||||||
end,
|
end,
|
||||||
groups = Item#roster.groups}.
|
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) ->
|
decode_item(Item, R, Managed) ->
|
||||||
R#roster{jid = jid:tolower(Item#roster_item.jid),
|
R#roster{jid = jid:tolower(Item#roster_item.jid),
|
||||||
name = Item#roster_item.name,
|
name = Item#roster_item.name,
|
||||||
subscription = case Item#roster_item.subscription of
|
subscription = case Item#roster_item.subscription of
|
||||||
remove -> remove;
|
|
||||||
Sub when Managed -> Sub;
|
Sub when Managed -> Sub;
|
||||||
_ -> R#roster.subscription
|
_ -> R#roster.subscription
|
||||||
end,
|
end,
|
||||||
@ -329,17 +369,6 @@ get_roster_by_jid_t(LUser, LServer, LJID) ->
|
|||||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||||
Mod:get_roster_by_jid(LUser, LServer, LJID).
|
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,
|
process_iq_set(#iq{from = From, to = To, id = Id,
|
||||||
sub_els = [#roster_query{items = QueryItems}]} = IQ) ->
|
sub_els = [#roster_query{items = QueryItems}]} = IQ) ->
|
||||||
Managed = is_managed_from_id(Id),
|
Managed = is_managed_from_id(Id),
|
||||||
@ -515,8 +544,7 @@ process_subscription(Direction, User, Server, JID1,
|
|||||||
{Subscription, Pending} ->
|
{Subscription, Pending} ->
|
||||||
NewItem = Item#roster{subscription = Subscription,
|
NewItem = Item#roster{subscription = Subscription,
|
||||||
ask = Pending,
|
ask = Pending,
|
||||||
askmessage =
|
askmessage = AskMessage},
|
||||||
iolist_to_binary(AskMessage)},
|
|
||||||
roster_subscribe_t(LUser, LServer, LJID, NewItem),
|
roster_subscribe_t(LUser, LServer, LJID, NewItem),
|
||||||
case roster_version_on_db(LServer) of
|
case roster_version_on_db(LServer) of
|
||||||
true -> write_roster_version_t(LUser, LServer);
|
true -> write_roster_version_t(LUser, LServer);
|
||||||
@ -730,10 +758,8 @@ del_roster_t(LUser, LServer, LJID) ->
|
|||||||
Mod:del_roster(LUser, LServer, LJID).
|
Mod:del_roster(LUser, LServer, LJID).
|
||||||
|
|
||||||
process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) ->
|
process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) ->
|
||||||
JID = {JID1#jid.user, JID1#jid.server,
|
JID = {JID1#jid.user, JID1#jid.server, <<>>},
|
||||||
JID1#jid.resource},
|
LJID = {JID1#jid.luser, JID1#jid.lserver, <<>>},
|
||||||
LJID = {JID1#jid.luser, JID1#jid.lserver,
|
|
||||||
JID1#jid.lresource},
|
|
||||||
Item = #roster{usj = {LUser, LServer, LJID},
|
Item = #roster{usj = {LUser, LServer, LJID},
|
||||||
us = {LUser, LServer}, jid = JID},
|
us = {LUser, LServer}, jid = JID},
|
||||||
Item2 = decode_item(QueryItem, Item, _Managed = true),
|
Item2 = decode_item(QueryItem, Item, _Managed = true),
|
||||||
@ -1046,6 +1072,10 @@ is_managed_from_id(<<"roster-remotely-managed">>) ->
|
|||||||
is_managed_from_id(_Id) ->
|
is_managed_from_id(_Id) ->
|
||||||
false.
|
false.
|
||||||
|
|
||||||
|
has_duplicated_groups(Groups) ->
|
||||||
|
GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]),
|
||||||
|
not (length(GroupsPrep) == length(Groups)).
|
||||||
|
|
||||||
export(LServer) ->
|
export(LServer) ->
|
||||||
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
Mod = gen_mod:db_mod(LServer, ?MODULE),
|
||||||
Mod:export(LServer).
|
Mod:export(LServer).
|
||||||
|
@ -394,7 +394,7 @@ db_tests(riak) ->
|
|||||||
auth_md5,
|
auth_md5,
|
||||||
presence_broadcast,
|
presence_broadcast,
|
||||||
last,
|
last,
|
||||||
roster_get,
|
roster_tests:single_cases(),
|
||||||
private,
|
private,
|
||||||
privacy_tests:single_cases(),
|
privacy_tests:single_cases(),
|
||||||
vcard,
|
vcard,
|
||||||
@ -402,9 +402,7 @@ db_tests(riak) ->
|
|||||||
test_unregister]},
|
test_unregister]},
|
||||||
muc_tests:master_slave_cases(),
|
muc_tests:master_slave_cases(),
|
||||||
privacy_tests:master_slave_cases(),
|
privacy_tests:master_slave_cases(),
|
||||||
{test_roster_subscribe, [parallel],
|
roster_tests:master_slave_cases(),
|
||||||
[roster_subscribe_master,
|
|
||||||
roster_subscribe_slave]},
|
|
||||||
{test_flex_offline, [sequence],
|
{test_flex_offline, [sequence],
|
||||||
[flex_offline_master, flex_offline_slave]},
|
[flex_offline_master, flex_offline_slave]},
|
||||||
{test_offline, [sequence],
|
{test_offline, [sequence],
|
||||||
@ -412,10 +410,7 @@ db_tests(riak) ->
|
|||||||
{test_announce, [sequence],
|
{test_announce, [sequence],
|
||||||
[announce_master, announce_slave]},
|
[announce_master, announce_slave]},
|
||||||
{test_vcard_xupdate, [parallel],
|
{test_vcard_xupdate, [parallel],
|
||||||
[vcard_xupdate_master, vcard_xupdate_slave]},
|
[vcard_xupdate_master, vcard_xupdate_slave]}];
|
||||||
{test_roster_remove, [parallel],
|
|
||||||
[roster_remove_master,
|
|
||||||
roster_remove_slave]}];
|
|
||||||
db_tests(DB) when DB == mnesia; DB == redis ->
|
db_tests(DB) when DB == mnesia; DB == redis ->
|
||||||
[{single_user, [sequence],
|
[{single_user, [sequence],
|
||||||
[test_register,
|
[test_register,
|
||||||
@ -424,8 +419,7 @@ db_tests(DB) when DB == mnesia; DB == redis ->
|
|||||||
auth_md5,
|
auth_md5,
|
||||||
presence_broadcast,
|
presence_broadcast,
|
||||||
last,
|
last,
|
||||||
roster_get,
|
roster_tests:single_cases(),
|
||||||
roster_ver,
|
|
||||||
private,
|
private,
|
||||||
privacy_tests:single_cases(),
|
privacy_tests:single_cases(),
|
||||||
vcard,
|
vcard,
|
||||||
@ -435,11 +429,9 @@ db_tests(DB) when DB == mnesia; DB == redis ->
|
|||||||
muc_tests:master_slave_cases(),
|
muc_tests:master_slave_cases(),
|
||||||
privacy_tests:master_slave_cases(),
|
privacy_tests:master_slave_cases(),
|
||||||
pubsub_multiple_tests(),
|
pubsub_multiple_tests(),
|
||||||
|
roster_tests:master_slave_cases(),
|
||||||
{test_mix, [parallel],
|
{test_mix, [parallel],
|
||||||
[mix_master, mix_slave]},
|
[mix_master, mix_slave]},
|
||||||
{test_roster_subscribe, [parallel],
|
|
||||||
[roster_subscribe_master,
|
|
||||||
roster_subscribe_slave]},
|
|
||||||
{test_flex_offline, [sequence],
|
{test_flex_offline, [sequence],
|
||||||
[flex_offline_master, flex_offline_slave]},
|
[flex_offline_master, flex_offline_slave]},
|
||||||
{test_offline, [sequence],
|
{test_offline, [sequence],
|
||||||
@ -457,10 +449,7 @@ db_tests(DB) when DB == mnesia; DB == redis ->
|
|||||||
{test_announce, [sequence],
|
{test_announce, [sequence],
|
||||||
[announce_master, announce_slave]},
|
[announce_master, announce_slave]},
|
||||||
{test_vcard_xupdate, [parallel],
|
{test_vcard_xupdate, [parallel],
|
||||||
[vcard_xupdate_master, vcard_xupdate_slave]},
|
[vcard_xupdate_master, vcard_xupdate_slave]}];
|
||||||
{test_roster_remove, [parallel],
|
|
||||||
[roster_remove_master,
|
|
||||||
roster_remove_slave]}];
|
|
||||||
db_tests(_) ->
|
db_tests(_) ->
|
||||||
%% No support for carboncopy
|
%% No support for carboncopy
|
||||||
[{single_user, [sequence],
|
[{single_user, [sequence],
|
||||||
@ -470,8 +459,7 @@ db_tests(_) ->
|
|||||||
auth_md5,
|
auth_md5,
|
||||||
presence_broadcast,
|
presence_broadcast,
|
||||||
last,
|
last,
|
||||||
roster_get,
|
roster_tests:single_cases(),
|
||||||
roster_ver,
|
|
||||||
private,
|
private,
|
||||||
privacy_tests:single_cases(),
|
privacy_tests:single_cases(),
|
||||||
vcard,
|
vcard,
|
||||||
@ -481,11 +469,9 @@ db_tests(_) ->
|
|||||||
muc_tests:master_slave_cases(),
|
muc_tests:master_slave_cases(),
|
||||||
privacy_tests:master_slave_cases(),
|
privacy_tests:master_slave_cases(),
|
||||||
pubsub_multiple_tests(),
|
pubsub_multiple_tests(),
|
||||||
|
roster_tests:master_slave_cases(),
|
||||||
{test_mix, [parallel],
|
{test_mix, [parallel],
|
||||||
[mix_master, mix_slave]},
|
[mix_master, mix_slave]},
|
||||||
{test_roster_subscribe, [parallel],
|
|
||||||
[roster_subscribe_master,
|
|
||||||
roster_subscribe_slave]},
|
|
||||||
{test_flex_offline, [sequence],
|
{test_flex_offline, [sequence],
|
||||||
[flex_offline_master, flex_offline_slave]},
|
[flex_offline_master, flex_offline_slave]},
|
||||||
{test_offline, [sequence],
|
{test_offline, [sequence],
|
||||||
@ -499,10 +485,7 @@ db_tests(_) ->
|
|||||||
{test_announce, [sequence],
|
{test_announce, [sequence],
|
||||||
[announce_master, announce_slave]},
|
[announce_master, announce_slave]},
|
||||||
{test_vcard_xupdate, [parallel],
|
{test_vcard_xupdate, [parallel],
|
||||||
[vcard_xupdate_master, vcard_xupdate_slave]},
|
[vcard_xupdate_master, vcard_xupdate_slave]}].
|
||||||
{test_roster_remove, [parallel],
|
|
||||||
[roster_remove_master,
|
|
||||||
roster_remove_slave]}].
|
|
||||||
|
|
||||||
ldap_tests() ->
|
ldap_tests() ->
|
||||||
[{ldap_tests, [sequence],
|
[{ldap_tests, [sequence],
|
||||||
@ -862,33 +845,26 @@ test_bind(Config) ->
|
|||||||
test_open_session(Config) ->
|
test_open_session(Config) ->
|
||||||
disconnect(open_session(Config, true)).
|
disconnect(open_session(Config, true)).
|
||||||
|
|
||||||
roster_get(Config) ->
|
roster_feature_enabled(Config) ->
|
||||||
#iq{type = result, sub_els = [#roster_query{items = []}]} =
|
roster_tests:feature_enabled(Config).
|
||||||
send_recv(Config, #iq{type = get, sub_els = [#roster_query{}]}),
|
roster_iq_set_many_items(Config) ->
|
||||||
disconnect(Config).
|
roster_tests:iq_set_many_items(Config).
|
||||||
|
roster_iq_set_duplicated_groups(Config) ->
|
||||||
roster_ver(Config) ->
|
roster_tests:iq_set_duplicated_groups(Config).
|
||||||
%% Get initial "ver"
|
roster_iq_set_ask(Config) ->
|
||||||
#iq{type = result, sub_els = [#roster_query{ver = Ver1, items = []}]} =
|
roster_tests:iq_set_ask(Config).
|
||||||
send_recv(Config, #iq{type = get,
|
roster_iq_get_item(Config) ->
|
||||||
sub_els = [#roster_query{ver = <<"">>}]}),
|
roster_tests:iq_get_item(Config).
|
||||||
%% Should receive empty IQ-result
|
roster_iq_unexpected_element(Config) ->
|
||||||
#iq{type = result, sub_els = []} =
|
roster_tests:iq_unexpected_element(Config).
|
||||||
send_recv(Config, #iq{type = get,
|
roster_set_item(Config) ->
|
||||||
sub_els = [#roster_query{ver = Ver1}]}),
|
roster_tests:set_item(Config).
|
||||||
%% Attempting to subscribe to server's JID
|
roster_version(Config) ->
|
||||||
send(Config, #presence{type = subscribe, to = server_jid(Config)}),
|
roster_tests:version(Config).
|
||||||
%% Receive a single roster push with the new "ver"
|
roster_subscribe_master(Config) ->
|
||||||
#iq{type = set, sub_els = [#roster_query{ver = Ver2}]} = recv_iq(Config),
|
roster_tests:subscribe_master(Config).
|
||||||
%% Requesting roster with the previous "ver". Should receive Ver2 again
|
roster_subscribe_slave(Config) ->
|
||||||
#iq{type = result, sub_els = [#roster_query{ver = Ver2}]} =
|
roster_tests:subscribe_slave(Config).
|
||||||
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).
|
|
||||||
|
|
||||||
codec_failure(Config) ->
|
codec_failure(Config) ->
|
||||||
JID = my_jid(Config),
|
JID = my_jid(Config),
|
||||||
@ -2043,148 +2019,6 @@ mix_slave(Config) ->
|
|||||||
disconnect = get_event(Config),
|
disconnect = get_event(Config),
|
||||||
disconnect(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) ->
|
proxy65_master(Config) ->
|
||||||
Proxy = proxy_jid(Config),
|
Proxy = proxy_jid(Config),
|
||||||
MyJID = my_jid(Config),
|
MyJID = my_jid(Config),
|
||||||
|
527
test/roster_tests.erl
Normal file
527
test/roster_tests.erl
Normal 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).
|
@ -86,6 +86,7 @@ init_config(Config) ->
|
|||||||
{stream_from, <<"">>},
|
{stream_from, <<"">>},
|
||||||
{db_xmlns, <<"">>},
|
{db_xmlns, <<"">>},
|
||||||
{mechs, []},
|
{mechs, []},
|
||||||
|
{rosterver, false},
|
||||||
{lang, <<"en">>},
|
{lang, <<"en">>},
|
||||||
{base_dir, BaseDir},
|
{base_dir, BaseDir},
|
||||||
{socket, undefined},
|
{socket, undefined},
|
||||||
@ -421,6 +422,8 @@ wait_auth_SASL_result(Config, ShouldFail) ->
|
|||||||
set_opt(sm, true, ConfigAcc);
|
set_opt(sm, true, ConfigAcc);
|
||||||
(#feature_csi{}, ConfigAcc) ->
|
(#feature_csi{}, ConfigAcc) ->
|
||||||
set_opt(csi, true, ConfigAcc);
|
set_opt(csi, true, ConfigAcc);
|
||||||
|
(#rosterver_feature{}, ConfigAcc) ->
|
||||||
|
set_opt(rosterver, true, ConfigAcc);
|
||||||
(_, ConfigAcc) ->
|
(_, ConfigAcc) ->
|
||||||
ConfigAcc
|
ConfigAcc
|
||||||
end, Config, Fs);
|
end, Config, Fs);
|
||||||
@ -674,26 +677,32 @@ set_opt(Opt, Val, Config) ->
|
|||||||
[{Opt, Val}|lists:keydelete(Opt, 1, Config)].
|
[{Opt, Val}|lists:keydelete(Opt, 1, Config)].
|
||||||
|
|
||||||
wait_for_master(Config) ->
|
wait_for_master(Config) ->
|
||||||
put_event(Config, slave_ready),
|
put_event(Config, peer_ready),
|
||||||
case get_event(Config) of
|
case get_event(Config) of
|
||||||
master_ready ->
|
peer_ready ->
|
||||||
ok;
|
ok;
|
||||||
Other ->
|
Other ->
|
||||||
suite:match_failure([Other], [master_ready])
|
suite:match_failure(Other, peer_ready)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
wait_for_slave(Config) ->
|
wait_for_slave(Config) ->
|
||||||
put_event(Config, master_ready),
|
put_event(Config, peer_ready),
|
||||||
case get_event(Config) of
|
case get_event(Config) of
|
||||||
slave_ready ->
|
peer_ready ->
|
||||||
ok;
|
ok;
|
||||||
Other ->
|
Other ->
|
||||||
suite:match_failure([Other], [slave_ready])
|
suite:match_failure(Other, peer_ready)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
make_iq_result(#iq{from = From} = IQ) ->
|
make_iq_result(#iq{from = From} = IQ) ->
|
||||||
IQ#iq{type = result, to = From, from = undefined, sub_els = []}.
|
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) ->
|
set_roster(Config, Subscription, Groups) ->
|
||||||
MyJID = my_jid(Config),
|
MyJID = my_jid(Config),
|
||||||
{U, S, _} = jid:tolower(MyJID),
|
{U, S, _} = jid:tolower(MyJID),
|
||||||
@ -710,15 +719,21 @@ set_roster(Config, Subscription, Groups) ->
|
|||||||
Config.
|
Config.
|
||||||
|
|
||||||
del_roster(Config) ->
|
del_roster(Config) ->
|
||||||
|
del_roster(Config, ?config(peer, Config)).
|
||||||
|
|
||||||
|
del_roster(Config, PeerJID) ->
|
||||||
MyJID = my_jid(Config),
|
MyJID = my_jid(Config),
|
||||||
{U, S, _} = jid:tolower(MyJID),
|
{U, S, _} = jid:tolower(MyJID),
|
||||||
PeerJID = ?config(peer, Config),
|
|
||||||
PeerBareJID = jid:remove_resource(PeerJID),
|
PeerBareJID = jid:remove_resource(PeerJID),
|
||||||
PeerLJID = jid:tolower(PeerBareJID),
|
PeerLJID = jid:tolower(PeerBareJID),
|
||||||
ct:comment("Removing ~s from roster", [jid:to_string(PeerBareJID)]),
|
ct:comment("Removing ~s from roster", [jid:to_string(PeerBareJID)]),
|
||||||
{atomic, _} = mod_roster:del_roster(U, S, PeerLJID),
|
{atomic, _} = mod_roster:del_roster(U, S, PeerLJID),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
get_roster(Config) ->
|
||||||
|
{LUser, LServer, _} = jid:tolower(my_jid(Config)),
|
||||||
|
mod_roster:get_roster(LUser, LServer).
|
||||||
|
|
||||||
receiver(NS, Owner) ->
|
receiver(NS, Owner) ->
|
||||||
MRef = erlang:monitor(process, Owner),
|
MRef = erlang:monitor(process, Owner),
|
||||||
receiver(NS, Owner, MRef).
|
receiver(NS, Owner, MRef).
|
||||||
|
Loading…
Reference in New Issue
Block a user