]> granicus.if.org Git - ejabberd/commitdiff
Add roster tests
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Mon, 7 Nov 2016 07:10:57 +0000 (10:10 +0300)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Mon, 7 Nov 2016 07:10:57 +0000 (10:10 +0300)
src/ejabberd_sm.erl
src/mod_roster.erl
test/ejabberd_SUITE.erl
test/roster_tests.erl [new file with mode: 0644]
test/suite.erl

index f6d0e765d337b5390d3d7c518eb2552b2912488f..6f6a196e56c50a39a4037d379669bb623c7585c4 100644 (file)
@@ -442,135 +442,96 @@ online(Sessions) ->
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 -spec do_route(jid(), jid(), stanza() | broadcast()) -> any().
+do_route(From, #jid{lresource = <<"">>} = To, {broadcast, _} = Packet) ->
+    ?DEBUG("processing broadcast to bare JID: ~p", [Packet]),
+    lists:foreach(
+      fun(R) ->
+             do_route(From, jid:replace_resource(To, R), Packet)
+      end, get_user_resources(To#jid.user, To#jid.server));
 do_route(From, To, {broadcast, _} = Packet) ->
-    case To#jid.lresource of
-        <<"">> ->
-            lists:foreach(fun(R) ->
-                                  do_route(From,
-                                           jid:replace_resource(To, R),
-                                           Packet)
-                          end,
-                          get_user_resources(To#jid.user, To#jid.server));
-        _ ->
-            {U, S, R} = jid:tolower(To),
-           Mod = get_sm_backend(S),
-           case online(Mod:get_sessions(U, S, R)) of
-                [] ->
-                    ?DEBUG("packet dropped~n", []);
-                Ss ->
-                    Session = lists:max(Ss),
-                    Pid = element(2, Session#session.sid),
-                    ?DEBUG("sending to process ~p~n", [Pid]),
-                    Pid ! {route, From, To, Packet}
-            end
+    ?DEBUG("processing broadcast to full JID: ~p", [Packet]),
+    {U, S, R} = jid:tolower(To),
+    Mod = get_sm_backend(S),
+    case online(Mod:get_sessions(U, S, R)) of
+       [] ->
+           ?DEBUG("dropping broadcast to unavailable resourse: ~p", [Packet]);
+       Ss ->
+           Session = lists:max(Ss),
+           Pid = element(2, Session#session.sid),
+           ?DEBUG("sending to process ~p: ~p", [Pid, Packet]),
+           Pid ! {route, From, To, Packet}
     end;
-do_route(From, To, Packet) ->
-    ?DEBUG("session manager~n\tfrom ~p~n\tto ~p~n\tpacket "
-          "~P~n",
-          [From, To, Packet, 8]),
+do_route(From, To, #presence{type = T, status = Status} = Packet)
+  when T == subscribe; T == subscribed; T == unsubscribe; T == unsubscribed ->
+    ?DEBUG("processing subscription:~n~s", [xmpp:pp(Packet)]),
     #jid{user = User, server = Server,
-        luser = LUser, lserver = LServer, lresource = LResource} = To,
-    Lang = xmpp:get_lang(Packet),
-    case LResource of
-      <<"">> ->
-         case Packet of
-           #presence{type = T, status = Status} ->
-               {Pass, _Subsc} = case T of
-                                  subscribe ->
-                                      Reason = xmpp:get_text(Status),
-                                      {is_privacy_allow(From, To, Packet)
-                                         andalso
-                                         ejabberd_hooks:run_fold(roster_in_subscription,
-                                                                 LServer,
-                                                                 false,
-                                                                 [User, Server,
-                                                                  From,
-                                                                  subscribe,
-                                                                  Reason]),
-                                       true};
-                                  subscribed ->
-                                      {is_privacy_allow(From, To, Packet)
-                                         andalso
-                                         ejabberd_hooks:run_fold(roster_in_subscription,
-                                                                 LServer,
-                                                                 false,
-                                                                 [User, Server,
-                                                                  From,
-                                                                  subscribed,
-                                                                  <<"">>]),
-                                       true};
-                                  unsubscribe ->
-                                      {is_privacy_allow(From, To, Packet)
-                                         andalso
-                                         ejabberd_hooks:run_fold(roster_in_subscription,
-                                                                 LServer,
-                                                                 false,
-                                                                 [User, Server,
-                                                                  From,
-                                                                  unsubscribe,
-                                                                  <<"">>]),
-                                       true};
-                                  unsubscribed ->
-                                      {is_privacy_allow(From, To, Packet)
-                                         andalso
-                                         ejabberd_hooks:run_fold(roster_in_subscription,
-                                                                 LServer,
-                                                                 false,
-                                                                 [User, Server,
-                                                                  From,
-                                                                  unsubscribed,
-                                                                  <<"">>]),
-                                       true};
-                                  _ -> {true, false}
-                                end,
-               if Pass ->
-                      PResources = get_user_present_resources(LUser, LServer),
-                      lists:foreach(fun ({_, R}) ->
-                                            do_route(From,
-                                                     jid:replace_resource(To,
-                                                                               R),
-                                                     Packet)
-                                    end,
-                                    PResources);
-                  true -> ok
-               end;
-             #message{type = T} when T == chat; T == headline; T == normal ->
-                 route_message(From, To, Packet, T);
-             #message{type = groupchat} ->
-                 ErrTxt = <<"User session not found">>,
-                 Err = xmpp:make_error(
-                         Packet, xmpp:err_service_unavailable(ErrTxt, Lang)),
-                 ejabberd_router:route(To, From, Err);
-             #iq{} -> process_iq(From, To, Packet);
-             _ -> ok
-         end;
-      _ ->
-       Mod = get_sm_backend(LServer),
-       case online(Mod:get_sessions(LUser, LServer, LResource)) of
-           [] ->
-               case Packet of
-                   #message{type = T} when T == chat; T == normal ->
-                       route_message(From, To, Packet, T);
-                   #message{type = groupchat} ->
-                       ErrTxt = <<"User session not found">>,
-                       Err = xmpp:make_error(
-                               Packet,
-                               xmpp:err_service_unavailable(ErrTxt, Lang)),
-                       ejabberd_router:route(To, From, Err);
-                   #iq{type = T} when T == get; T == set ->
-                       ErrTxt = <<"User session not found">>,
-                       Err = xmpp:make_error(
-                               Packet,
-                               xmpp:err_service_unavailable(ErrTxt, Lang)),
-                       ejabberd_router:route(To, From, Err);
-                   _ -> ?DEBUG("packet dropped~n", [])
-               end;
-           Ss ->
-               Session = lists:max(Ss),
-               Pid = element(2, Session#session.sid),
-               ?DEBUG("sending to process ~p~n", [Pid]),
-               Pid ! {route, From, To, Packet}
-         end
+        luser = LUser, lserver = LServer} = To,
+    Reason = if T == subscribe -> xmpp:get_text(Status);
+               true -> <<"">>
+            end,
+    case is_privacy_allow(From, To, Packet) andalso
+       ejabberd_hooks:run_fold(
+         roster_in_subscription,
+         LServer, false,
+         [User, Server, From, T, Reason]) of
+       true ->
+           Mod = get_sm_backend(LServer),
+           lists:foreach(
+             fun(#session{sid = SID, usr = {_, _, R},
+                          priority = Prio}) when is_integer(Prio) ->
+                     Pid = element(2, SID),
+                     ?DEBUG("sending to process ~p:~n~s",
+                            [Pid, xmpp:pp(Packet)]),
+                     Pid ! {route, From, jid:replace_resource(To, R), Packet};
+                (_) ->
+                     ok
+             end, online(Mod:get_sessions(LUser, LServer)));
+       false ->
+           ok
+    end;
+do_route(From, #jid{lresource = <<"">>} = To, #presence{} = Packet) ->
+    ?DEBUG("processing presence to bare JID:~n~s", [xmpp:pp(Packet)]),
+    {LUser, LServer, _} = jid:tolower(To),
+    lists:foreach(
+      fun({_, R}) ->
+             do_route(From, jid:replace_resource(To, R), Packet)
+      end, get_user_present_resources(LUser, LServer));
+do_route(From, #jid{lresource = <<"">>} = To, #message{type = T} = Packet) ->
+    ?DEBUG("processing message to bare JID:~n~s", [xmpp:pp(Packet)]),
+    if T == chat; T == headline; T == normal ->
+           route_message(From, To, Packet, T);
+       true ->
+           Lang = xmpp:get_lang(Packet),
+           ErrTxt = <<"User session not found">>,
+           Err = xmpp:err_service_unavailable(ErrTxt, Lang),
+           ejabberd_router:route_error(To, From, Packet, Err)
+    end;
+do_route(From, #jid{lresource = <<"">>} = To, #iq{} = Packet) ->
+    ?DEBUG("processing IQ to bare JID:~n~s", [xmpp:pp(Packet)]),
+    process_iq(From, To, Packet);
+do_route(From, To, Packet) ->
+    ?DEBUG("processing packet to full JID:~n~s", [xmpp:pp(Packet)]),
+    {LUser, LServer, LResource} = jid:tolower(To),
+    Mod = get_sm_backend(LServer),
+    case online(Mod:get_sessions(LUser, LServer, LResource)) of
+       [] ->
+           case Packet of
+               #message{type = T} when T == chat; T == normal ->
+                   route_message(From, To, Packet, T);
+               #presence{} ->
+                   ?DEBUG("dropping presence to unavalable resource:~n~s",
+                          [xmpp:pp(Packet)]);
+               _ ->
+                   Lang = xmpp:get_lang(Packet),
+                   ErrTxt = <<"User session not found">>,
+                   Err = xmpp:err_service_unavailable(ErrTxt, Lang),
+                   ejabberd_router:route_error(To, From, Packet, Err)
+           end;
+       Ss ->
+           Session = lists:max(Ss),
+           Pid = element(2, Session#session.sid),
+           ?DEBUG("sending to process ~p:~n~s", [Pid, xmpp:pp(Packet)]),
+           Pid ! {route, From, To, Packet}
     end.
 
 %% The default list applies to the user as a whole,
index 423fe9e0eba12dc0b41c21a70f8d7ce0e57848f7..fa27f866cfb6bd5dd2b04674a8e02b2c27f0e284 100644 (file)
@@ -142,21 +142,54 @@ depends(_Host, _Opts) ->
 process_iq(#iq{from = #jid{luser = <<"">>},
               to = #jid{resource = <<"">>}} = IQ) ->
     process_iq_manager(IQ);
-
-process_iq(#iq{from = 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 =
@@ -315,11 +348,18 @@ encode_item(Item) ->
                       end,
                 groups = Item#roster.groups}.
 
+decode_item(#roster_item{subscription = remove} = Item, R, _) ->
+    R#roster{jid = jid:tolower(Item#roster_item.jid),
+            name = <<"">>,
+            subscription = remove,
+            ask = none,
+            groups = [],
+            askmessage = <<"">>,
+            xs = []};
 decode_item(Item, R, Managed) ->
     R#roster{jid = jid:tolower(Item#roster_item.jid),
             name = Item#roster_item.name,
             subscription = case Item#roster_item.subscription of
-                               remove -> remove;
                                Sub when Managed -> Sub;
                                _ -> R#roster.subscription
                            end,
@@ -329,17 +369,6 @@ get_roster_by_jid_t(LUser, LServer, LJID) ->
     Mod = gen_mod:db_mod(LServer, ?MODULE),
     Mod:get_roster_by_jid(LUser, LServer, LJID).
 
-try_process_iq_set(#iq{from = From, lang = Lang} = IQ) ->
-    #jid{server = Server} = From,
-    Access = gen_mod:get_module_opt(Server, ?MODULE, access, fun(A) -> A end, all),
-    case acl:match_rule(Server, Access, From) of
-       deny ->
-           Txt = <<"Denied by ACL">>,
-           xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
-       allow ->
-           process_iq_set(IQ)
-    end.
-
 process_iq_set(#iq{from = From, to = To, id = Id,
                   sub_els = [#roster_query{items = QueryItems}]} = IQ) ->
     Managed = is_managed_from_id(Id),
@@ -515,8 +544,7 @@ process_subscription(Direction, User, Server, JID1,
                  {Subscription, Pending} ->
                      NewItem = Item#roster{subscription = Subscription,
                                            ask = Pending,
-                                           askmessage =
-                                               iolist_to_binary(AskMessage)},
+                                           askmessage = AskMessage},
                      roster_subscribe_t(LUser, LServer, LJID, NewItem),
                      case roster_version_on_db(LServer) of
                        true -> write_roster_version_t(LUser, LServer);
@@ -730,10 +758,8 @@ del_roster_t(LUser, LServer, LJID) ->
     Mod:del_roster(LUser, LServer, LJID).
 
 process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) ->
-    JID = {JID1#jid.user, JID1#jid.server,
-          JID1#jid.resource},
-    LJID = {JID1#jid.luser, JID1#jid.lserver,
-           JID1#jid.lresource},
+    JID = {JID1#jid.user, JID1#jid.server, <<>>},
+    LJID = {JID1#jid.luser, JID1#jid.lserver, <<>>},
     Item = #roster{usj = {LUser, LServer, LJID},
                   us = {LUser, LServer}, jid = JID},
     Item2 = decode_item(QueryItem, Item, _Managed = true),
@@ -1046,6 +1072,10 @@ is_managed_from_id(<<"roster-remotely-managed">>) ->
 is_managed_from_id(_Id) ->
     false.
 
+has_duplicated_groups(Groups) ->
+    GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]),
+    not (length(GroupsPrep) == length(Groups)).
+
 export(LServer) ->
     Mod = gen_mod:db_mod(LServer, ?MODULE),
     Mod:export(LServer).
index 59936352b4699ccb66655596d90d54b384e85254..a13d801ea6af6eadd3f3fb54b5741a851c15daa8 100644 (file)
@@ -394,7 +394,7 @@ db_tests(riak) ->
        auth_md5,
        presence_broadcast,
        last,
-       roster_get,
+       roster_tests:single_cases(),
        private,
        privacy_tests:single_cases(),
        vcard,
@@ -402,9 +402,7 @@ db_tests(riak) ->
        test_unregister]},
      muc_tests:master_slave_cases(),
      privacy_tests:master_slave_cases(),
-     {test_roster_subscribe, [parallel],
-      [roster_subscribe_master,
-       roster_subscribe_slave]},
+     roster_tests:master_slave_cases(),
      {test_flex_offline, [sequence],
       [flex_offline_master, flex_offline_slave]},
      {test_offline, [sequence],
@@ -412,10 +410,7 @@ db_tests(riak) ->
      {test_announce, [sequence],
       [announce_master, announce_slave]},
      {test_vcard_xupdate, [parallel],
-      [vcard_xupdate_master, vcard_xupdate_slave]},
-     {test_roster_remove, [parallel],
-      [roster_remove_master,
-       roster_remove_slave]}];
+      [vcard_xupdate_master, vcard_xupdate_slave]}];
 db_tests(DB) when DB == mnesia; DB == redis ->
     [{single_user, [sequence],
       [test_register,
@@ -424,8 +419,7 @@ db_tests(DB) when DB == mnesia; DB == redis ->
        auth_md5,
        presence_broadcast,
        last,
-       roster_get,
-       roster_ver,
+       roster_tests:single_cases(),
        private,
        privacy_tests:single_cases(),
        vcard,
@@ -435,11 +429,9 @@ db_tests(DB) when DB == mnesia; DB == redis ->
      muc_tests:master_slave_cases(),
      privacy_tests:master_slave_cases(),
      pubsub_multiple_tests(),
+     roster_tests:master_slave_cases(),
      {test_mix, [parallel],
       [mix_master, mix_slave]},
-     {test_roster_subscribe, [parallel],
-      [roster_subscribe_master,
-       roster_subscribe_slave]},
      {test_flex_offline, [sequence],
       [flex_offline_master, flex_offline_slave]},
      {test_offline, [sequence],
@@ -457,10 +449,7 @@ db_tests(DB) when DB == mnesia; DB == redis ->
      {test_announce, [sequence],
       [announce_master, announce_slave]},
      {test_vcard_xupdate, [parallel],
-      [vcard_xupdate_master, vcard_xupdate_slave]},
-     {test_roster_remove, [parallel],
-      [roster_remove_master,
-       roster_remove_slave]}];
+      [vcard_xupdate_master, vcard_xupdate_slave]}];
 db_tests(_) ->
     %% No support for carboncopy
     [{single_user, [sequence],
@@ -470,8 +459,7 @@ db_tests(_) ->
        auth_md5,
        presence_broadcast,
        last,
-       roster_get,
-       roster_ver,
+       roster_tests:single_cases(),
        private,
        privacy_tests:single_cases(),
        vcard,
@@ -481,11 +469,9 @@ db_tests(_) ->
      muc_tests:master_slave_cases(),
      privacy_tests:master_slave_cases(),
      pubsub_multiple_tests(),
+     roster_tests:master_slave_cases(),
      {test_mix, [parallel],
       [mix_master, mix_slave]},
-     {test_roster_subscribe, [parallel],
-      [roster_subscribe_master,
-       roster_subscribe_slave]},
      {test_flex_offline, [sequence],
       [flex_offline_master, flex_offline_slave]},
      {test_offline, [sequence],
@@ -499,10 +485,7 @@ db_tests(_) ->
      {test_announce, [sequence],
       [announce_master, announce_slave]},
      {test_vcard_xupdate, [parallel],
-      [vcard_xupdate_master, vcard_xupdate_slave]},
-     {test_roster_remove, [parallel],
-      [roster_remove_master,
-       roster_remove_slave]}].
+      [vcard_xupdate_master, vcard_xupdate_slave]}].
 
 ldap_tests() ->
     [{ldap_tests, [sequence],
@@ -862,33 +845,26 @@ test_bind(Config) ->
 test_open_session(Config) ->
     disconnect(open_session(Config, true)).
 
-roster_get(Config) ->
-    #iq{type = result, sub_els = [#roster_query{items = []}]} =
-        send_recv(Config, #iq{type = get, sub_els = [#roster_query{}]}),
-    disconnect(Config).
-
-roster_ver(Config) ->
-    %% Get initial "ver"
-    #iq{type = result, sub_els = [#roster_query{ver = Ver1, items = []}]} =
-        send_recv(Config, #iq{type = get,
-                              sub_els = [#roster_query{ver = <<"">>}]}),
-    %% Should receive empty IQ-result
-    #iq{type = result, sub_els = []} =
-        send_recv(Config, #iq{type = get,
-                              sub_els = [#roster_query{ver = Ver1}]}),
-    %% Attempting to subscribe to server's JID
-    send(Config, #presence{type = subscribe, to = server_jid(Config)}),
-    %% Receive a single roster push with the new "ver"
-    #iq{type = set, sub_els = [#roster_query{ver = Ver2}]} = recv_iq(Config),
-    %% Requesting roster with the previous "ver". Should receive Ver2 again
-    #iq{type = result, sub_els = [#roster_query{ver = Ver2}]} =
-        send_recv(Config, #iq{type = get,
-                              sub_els = [#roster_query{ver = Ver1}]}),
-    %% Now requesting roster with the newest "ver". Should receive empty IQ.
-    #iq{type = result, sub_els = []} =
-        send_recv(Config, #iq{type = get,
-                              sub_els = [#roster_query{ver = Ver2}]}),
-    disconnect(Config).
+roster_feature_enabled(Config) ->
+    roster_tests:feature_enabled(Config).
+roster_iq_set_many_items(Config) ->
+    roster_tests:iq_set_many_items(Config).
+roster_iq_set_duplicated_groups(Config) ->
+    roster_tests:iq_set_duplicated_groups(Config).
+roster_iq_set_ask(Config) ->
+    roster_tests:iq_set_ask(Config).
+roster_iq_get_item(Config) ->
+    roster_tests:iq_get_item(Config).
+roster_iq_unexpected_element(Config) ->
+    roster_tests:iq_unexpected_element(Config).
+roster_set_item(Config) ->
+    roster_tests:set_item(Config).
+roster_version(Config) ->
+    roster_tests:version(Config).
+roster_subscribe_master(Config) ->
+    roster_tests:subscribe_master(Config).
+roster_subscribe_slave(Config) ->
+    roster_tests:subscribe_slave(Config).
 
 codec_failure(Config) ->
     JID = my_jid(Config),
@@ -2043,148 +2019,6 @@ mix_slave(Config) ->
     disconnect = get_event(Config),
     disconnect(Config).
 
-roster_subscribe_master(Config) ->
-    #presence{} = send_recv(Config, #presence{}),
-    wait_for_slave(Config),
-    Peer = ?config(peer, Config),
-    LPeer = jid:remove_resource(Peer),
-    send(Config, #presence{type = subscribe, to = LPeer}),
-    Push1 = #iq{type = set,
-                sub_els = [#roster_query{items = [#roster_item{
-                                                    ask = subscribe,
-                                                    subscription = none,
-                                                    jid = LPeer}]}]} =
-       recv_iq(Config),
-    send(Config, make_iq_result(Push1)),
-    #presence{type = subscribed, from = LPeer} = recv_presence(Config),
-    Push2 = #iq{type = set,
-               sub_els = [#roster_query{items = [#roster_item{
-                                                    subscription = to,
-                                                    jid = LPeer}]}]} =
-       recv_iq(Config),
-    send(Config, make_iq_result(Push2)),
-    #presence{type = available, from = Peer} = recv_presence(Config),
-    %% BUG: ejabberd sends previous push again. Is it ok?
-    Push3 = #iq{type = set,
-                sub_els = [#roster_query{items = [#roster_item{
-                                                    subscription = to,
-                                                    jid = LPeer}]}]} =
-       recv_iq(Config),
-    send(Config, make_iq_result(Push3)),
-    #presence{type = subscribe, from = LPeer} = recv_presence(Config),
-    send(Config, #presence{type = subscribed, to = LPeer}),
-    Push4 = #iq{type = set,
-                sub_els = [#roster_query{items = [#roster_item{
-                                                    subscription = both,
-                                                    jid = LPeer}]}]} =
-       recv_iq(Config),
-    send(Config, make_iq_result(Push4)),
-    %% Move into a group
-    Groups = [<<"A">>, <<"B">>],
-    Item = #roster_item{jid = LPeer, groups = Groups},
-    #iq{type = result, sub_els = []} =
-       send_recv(Config,
-                 #iq{type = set, sub_els = [#roster_query{items = [Item]}]}),
-    Push5 = #iq{type = set,
-               sub_els =
-                   [#roster_query{items = [#roster_item{
-                                              jid = LPeer,
-                                              subscription = both}]}]} =
-       recv_iq(Config),
-    send(Config, make_iq_result(Push5)),
-    #iq{sub_els = [#roster_query{items = [#roster_item{groups = G1}]}]} = Push5,
-    Groups = lists:sort(G1),
-    wait_for_slave(Config),
-    #presence{type = unavailable, from = Peer} = recv_presence(Config),
-    disconnect(Config).
-
-roster_subscribe_slave(Config) ->
-    #presence{} = send_recv(Config, #presence{}),
-    wait_for_master(Config),
-    Peer = ?config(master, Config),
-    LPeer = jid:remove_resource(Peer),
-    #presence{type = subscribe, from = LPeer} = recv_presence(Config),
-    send(Config, #presence{type = subscribed, to = LPeer}),
-    Push1 = #iq{type = set,
-                sub_els = [#roster_query{items = [#roster_item{
-                                                    subscription = from,
-                                                    jid = LPeer}]}]} =
-       recv_iq(Config),
-    send(Config, make_iq_result(Push1)),
-    send(Config, #presence{type = subscribe, to = LPeer}),
-    Push2 = #iq{type = set,
-                sub_els = [#roster_query{items = [#roster_item{
-                                                    ask = subscribe,
-                                                    subscription = from,
-                                                    jid = LPeer}]}]} =
-       recv_iq(Config),
-    send(Config, make_iq_result(Push2)),
-    #presence{type = subscribed, from = LPeer} = recv_presence(Config),
-    Push3 = #iq{type = set,
-               sub_els = [#roster_query{items = [#roster_item{
-                                                    subscription = both,
-                                                    jid = LPeer}]}]} =
-       recv_iq(Config),
-    send(Config, make_iq_result(Push3)),
-    #presence{type = available, from = Peer} = recv_presence(Config),
-    wait_for_master(Config),
-    disconnect(Config).
-
-roster_remove_master(Config) ->
-    MyJID = my_jid(Config),
-    Peer = ?config(slave, Config),
-    LPeer = jid:remove_resource(Peer),
-    Groups = [<<"A">>, <<"B">>],
-    wait_for_slave(Config),
-    #presence{from = MyJID, type = available} = send_recv(Config, #presence{}),
-    #presence{from = Peer, type = available} = recv_presence(Config),
-    %% The peer removed us from its roster.
-    {Push1, Push2, _, _, _} =
-        ?recv5(
-           %% TODO: I guess this can be optimized, we don't need
-           %% to send transient roster push with subscription = 'to'.
-           #iq{type = set,
-               sub_els =
-                   [#roster_query{items = [#roster_item{
-                                        jid = LPeer,
-                                        subscription = to}]}]},
-           #iq{type = set,
-               sub_els =
-                   [#roster_query{items = [#roster_item{
-                                        jid = LPeer,
-                                        subscription = none}]}]},
-           #presence{type = unsubscribe, from = LPeer},
-           #presence{type = unsubscribed, from = LPeer},
-           #presence{type = unavailable, from = Peer}),
-    send(Config, make_iq_result(Push1)),
-    send(Config, make_iq_result(Push2)),
-    #iq{sub_els = [#roster_query{items = [#roster_item{groups = G1}]}]} = Push1,
-    #iq{sub_els = [#roster_query{items = [#roster_item{groups = G2}]}]} = Push2,
-    Groups = lists:sort(G1), Groups = lists:sort(G2),
-    disconnect(Config).
-
-roster_remove_slave(Config) ->
-    MyJID = my_jid(Config),
-    Peer = ?config(master, Config),
-    LPeer = jid:remove_resource(Peer),
-    #presence{from = MyJID, type = available} = send_recv(Config, #presence{}),
-    wait_for_master(Config),
-    #presence{from = Peer, type = available} = recv_presence(Config),
-    %% Remove the peer from roster.
-    Item = #roster_item{jid = LPeer, subscription = remove},
-    #iq{type = result, sub_els = []} =
-       send_recv(Config, #iq{type = set,
-                             sub_els = [#roster_query{items = [Item]}]}),
-    Push = #iq{type = set,
-              sub_els =
-                  [#roster_query{items = [#roster_item{
-                                             jid = LPeer,
-                                             subscription = remove}]}]} =
-       recv_iq(Config),
-    #presence{type = unavailable, from = Peer} = recv_presence(Config),
-    send(Config, make_iq_result(Push)),
-    disconnect(Config).
-
 proxy65_master(Config) ->
     Proxy = proxy_jid(Config),
     MyJID = my_jid(Config),
diff --git a/test/roster_tests.erl b/test/roster_tests.erl
new file mode 100644 (file)
index 0000000..2d05709
--- /dev/null
@@ -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).
index 3bed6256271cb5ed6df42e75d63fef9ff7daccd9..f88ac5a5e1b3334bc2cf9b7dec825cc152086ac0 100644 (file)
@@ -86,6 +86,7 @@ init_config(Config) ->
      {stream_from, <<"">>},
      {db_xmlns, <<"">>},
      {mechs, []},
+     {rosterver, false},
      {lang, <<"en">>},
      {base_dir, BaseDir},
      {socket, undefined},
@@ -421,6 +422,8 @@ wait_auth_SASL_result(Config, ShouldFail) ->
                      set_opt(sm, true, ConfigAcc);
                 (#feature_csi{}, ConfigAcc) ->
                      set_opt(csi, true, ConfigAcc);
+                (#rosterver_feature{}, ConfigAcc) ->
+                     set_opt(rosterver, true, ConfigAcc);
                 (_, ConfigAcc) ->
                      ConfigAcc
              end, Config, Fs);
@@ -674,26 +677,32 @@ set_opt(Opt, Val, Config) ->
     [{Opt, Val}|lists:keydelete(Opt, 1, Config)].
 
 wait_for_master(Config) ->
-    put_event(Config, slave_ready),
+    put_event(Config, peer_ready),
     case get_event(Config) of
-       master_ready ->
+       peer_ready ->
            ok;
        Other ->
-           suite:match_failure([Other], [master_ready])
+           suite:match_failure(Other, peer_ready)
     end.
 
 wait_for_slave(Config) ->
-    put_event(Config, master_ready),
+    put_event(Config, peer_ready),
     case get_event(Config) of
-       slave_ready ->
+       peer_ready ->
            ok;
        Other ->
-           suite:match_failure([Other], [slave_ready])
+           suite:match_failure(Other, peer_ready)
     end.
 
 make_iq_result(#iq{from = From} = IQ) ->
     IQ#iq{type = result, to = From, from = undefined, sub_els = []}.
 
+self_presence(Config, Type) ->
+    MyJID = my_jid(Config),
+    ct:comment("Sending self-presence"),
+    #presence{type = Type, from = MyJID} =
+       send_recv(Config, #presence{type = Type}).
+
 set_roster(Config, Subscription, Groups) ->
     MyJID = my_jid(Config),
     {U, S, _} = jid:tolower(MyJID),
@@ -710,15 +719,21 @@ set_roster(Config, Subscription, Groups) ->
     Config.
 
 del_roster(Config) ->
+    del_roster(Config, ?config(peer, Config)).
+
+del_roster(Config, PeerJID) ->
     MyJID = my_jid(Config),
     {U, S, _} = jid:tolower(MyJID),
-    PeerJID = ?config(peer, Config),
     PeerBareJID = jid:remove_resource(PeerJID),
     PeerLJID = jid:tolower(PeerBareJID),
     ct:comment("Removing ~s from roster", [jid:to_string(PeerBareJID)]),
     {atomic, _} = mod_roster:del_roster(U, S, PeerLJID),
     Config.
 
+get_roster(Config) ->
+    {LUser, LServer, _} = jid:tolower(my_jid(Config)),
+    mod_roster:get_roster(LUser, LServer).
+
 receiver(NS, Owner) ->
     MRef = erlang:monitor(process, Owner),
     receiver(NS, Owner, MRef).