%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-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,
process_iq(#iq{from = #jid{luser = <<"">>},
to = #jid{resource = <<"">>}} = IQ) ->
process_iq_manager(IQ);
-
-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_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_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 =
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,
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),
{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);
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),
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).
auth_md5,
presence_broadcast,
last,
- roster_get,
+ roster_tests:single_cases(),
private,
privacy_tests:single_cases(),
vcard,
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],
{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,
auth_md5,
presence_broadcast,
last,
- roster_get,
- roster_ver,
+ roster_tests:single_cases(),
private,
privacy_tests:single_cases(),
vcard,
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],
{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],
auth_md5,
presence_broadcast,
last,
- roster_get,
- roster_ver,
+ roster_tests:single_cases(),
private,
privacy_tests:single_cases(),
vcard,
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],
{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],
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),
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),
--- /dev/null
+%%%-------------------------------------------------------------------
+%%% @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).
{stream_from, <<"">>},
{db_xmlns, <<"">>},
{mechs, []},
+ {rosterver, false},
{lang, <<"en">>},
{base_dir, BaseDir},
{socket, undefined},
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);
[{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),
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).