]> granicus.if.org Git - ejabberd/commitdiff
Add more MUC tests
authorEvgeniy Khramtsov <ekhramtsov@process-one.net>
Mon, 17 Oct 2016 10:37:23 +0000 (13:37 +0300)
committerEvgeniy Khramtsov <ekhramtsov@process-one.net>
Mon, 17 Oct 2016 10:37:23 +0000 (13:37 +0300)
src/gen_iq_handler.erl
src/mod_mam.erl
src/mod_muc.erl
src/mod_muc_room.erl
src/str.erl
test/ejabberd_SUITE.erl
test/ejabberd_SUITE_data/ejabberd.yml
test/muc_tests.erl [new file with mode: 0644]
test/suite.erl

index b8a44c96c9fe238a624180b3ffe7644e3f3c3388..8af2cb028d3fbeeb26978d27113f72f5970921f6 100644 (file)
@@ -156,7 +156,7 @@ process_iq(Module, Function, #iq{lang = Lang, sub_els = [El]} = IQ) ->
        %% TODO: move this 'conditional' decoding somewhere
        %% IQ handler should know *nothing* about vCards.
        Pkt = case xmpp:get_ns(El) of
-                 ?NS_VCARD -> El;
+                 ?NS_VCARD when Module == mod_vcard -> El;
                  _ -> xmpp:decode(El)
              end,
        Module:Function(IQ#iq{sub_els = [Pkt]})
index 1daae5aa20b988aafc31971df205bb3649580d32..cbd23ebde7a406a279feef22a33dd008edb06937 100644 (file)
@@ -772,28 +772,25 @@ select(_LServer, JidRequestor, JidArchive,
        {groupchat, _Role, #state{config = #config{mam = false},
                                 history = History}} = MsgType) ->
     #lqueue{len = L, queue = Q} = History,
-    {Msgs0, _} =
-       lists:mapfoldl(
-         fun({Nick, Pkt, _HaveSubject, UTCDateTime, _Size}, I) ->
-                 Now = datetime_to_now(UTCDateTime, I),
+    Msgs =
+       lists:flatmap(
+         fun({Nick, Pkt, _HaveSubject, Now, _Size}) ->
                  TS = now_to_usec(Now),
                  case match_interval(Now, Start, End) and
                      match_rsm(Now, RSM) of
                      true ->
-                         {[{integer_to_binary(TS), TS,
-                            msg_to_el(#archive_msg{
-                                         type = groupchat,
-                                         timestamp = Now,
-                                         peer = undefined,
-                                         nick = Nick,
-                                         packet = Pkt},
-                                      MsgType, JidRequestor, JidArchive)}],
-                          I+1};
+                         [{integer_to_binary(TS), TS,
+                           msg_to_el(#archive_msg{
+                                        type = groupchat,
+                                        timestamp = Now,
+                                        peer = undefined,
+                                        nick = Nick,
+                                        packet = Pkt},
+                                     MsgType, JidRequestor, JidArchive)}];
                      false ->
-                         {[], I+1}
+                         []
                  end
-         end, 0, queue:to_list(Q)),
-    Msgs = lists:flatten(Msgs0),
+         end, queue:to_list(Q)),
     case RSM of
        #rsm_set{max = Max, before = Before} when is_binary(Before) ->
            {NewMsgs, IsComplete} = filter_by_max(lists:reverse(Msgs), Max),
@@ -960,11 +957,6 @@ usec_to_now(Int) ->
     Sec = Secs rem 1000000,
     {MSec, Sec, USec}.
 
-datetime_to_now(DateTime, USecs) ->
-    Seconds = calendar:datetime_to_gregorian_seconds(DateTime) -
-       calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
-    {Seconds div 1000000, Seconds rem 1000000, USecs}.
-
 get_jids(undefined) ->
     [];
 get_jids(Js) ->
index 6b71d1e1d9b9d250d5f9b5ecd5d955005dce5a67..9c17643b87cbe700321ebd43be93ec4fda846fca 100644 (file)
@@ -377,7 +377,7 @@ do_route1(Host, ServerHost, Access, _HistorySize, _RoomShaper,
     end;
 do_route1(_Host, _ServerHost, _Access, _HistorySize, _RoomShaper,
          From, #jid{luser = <<"">>} = To, Packet, _DefRoomOpts) ->
-    Err = xmpp:err_item_not_found(),
+    Err = xmpp:err_service_unavailable(),
     ejabberd_router:route_error(To, From, Packet, Err);
 do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
          From, To, Packet, DefRoomOpts) ->
@@ -419,7 +419,7 @@ do_route1(Host, ServerHost, Access, HistorySize, RoomShaper,
     end.
 
 -spec process_vcard(iq()) -> iq().
-process_vcard(#iq{type = get, lang = Lang} = IQ) ->
+process_vcard(#iq{type = get, lang = Lang, sub_els = [#vcard_temp{}]} = IQ) ->
     Desc = translate:translate(Lang, <<"ejabberd MUC module">>),
     Copyright = <<"Copyright (c) 2003-2016 ProcessOne">>,
     xmpp:make_iq_result(
@@ -428,15 +428,19 @@ process_vcard(#iq{type = get, lang = Lang} = IQ) ->
                      desc = <<Desc/binary, $\n, Copyright/binary>>});
 process_vcard(#iq{type = set, lang = Lang} = IQ) ->
     Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
-    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)).
+    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
+process_vcard(#iq{lang = Lang} = IQ) ->
+    Txt = <<"No module is handling this query">>,
+    xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
 
 -spec process_register(iq()) -> iq().
-process_register(#iq{type = get, from = From, to = To, lang = Lang} = IQ) ->
+process_register(#iq{type = get, from = From, to = To, lang = Lang,
+                    sub_els = [#register{}]} = IQ) ->
     Host = To#jid.lserver,
     ServerHost = ejabberd_router:host_of_route(Host),
     xmpp:make_iq_result(IQ, iq_get_register_info(ServerHost, Host, From, Lang));
 process_register(#iq{type = set, from = From, to = To,
-                    lang = Lang, sub_els = [El]} = IQ) ->
+                    lang = Lang, sub_els = [El = #register{}]} = IQ) ->
     Host = To#jid.lserver,
     ServerHost = ejabberd_router:host_of_route(Host),
     case process_iq_register_set(ServerHost, Host, From, El, Lang) of
@@ -469,8 +473,12 @@ process_disco_info(#iq{type = get, to = To, lang = Lang,
       IQ, #disco_info{features = Features,
                      identities = [Identity],
                      xdata = X});
-process_disco_info(#iq{type = get, lang = Lang} = IQ) ->
-    xmpp:make_error(IQ, xmpp:err_item_not_found(<<"No info available">>, Lang)).
+process_disco_info(#iq{type = get, lang = Lang,
+                      sub_els = [#disco_info{}]} = IQ) ->
+    xmpp:make_error(IQ, xmpp:err_item_not_found(<<"Node not found">>, Lang));
+process_disco_info(#iq{lang = Lang} = IQ) ->
+    Txt = <<"No module is handling this query">>,
+    xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
 
 -spec process_disco_items(iq()) -> iq().
 process_disco_items(#iq{type = set, lang = Lang} = IQ) ->
@@ -481,13 +489,17 @@ process_disco_items(#iq{type = get, from = From, to = To, lang = Lang,
     Host = To#jid.lserver,
     xmpp:make_iq_result(
       IQ, #disco_items{node = Node,
-                      items = iq_disco_items(Host, From, Lang, Node, RSM)}).
+                      items = iq_disco_items(Host, From, Lang, Node, RSM)});
+process_disco_items(#iq{lang = Lang} = IQ) ->
+    Txt = <<"No module is handling this query">>,
+    xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
 
 -spec process_muc_unique(iq()) -> iq().
 process_muc_unique(#iq{type = set, lang = Lang} = IQ) ->
     Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
     xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
-process_muc_unique(#iq{from = From, type = get} = IQ) ->
+process_muc_unique(#iq{from = From, type = get,
+                      sub_els = [#muc_unique{}]} = IQ) ->
     Name = p1_sha:sha(term_to_binary([From, p1_time_compat:timestamp(),
                                      randoms:get_string()])),
     xmpp:make_iq_result(IQ, #muc_unique{name = Name}).
@@ -496,19 +508,22 @@ process_muc_unique(#iq{from = From, type = get} = IQ) ->
 process_mucsub(#iq{type = set, lang = Lang} = IQ) ->
     Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
     xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
-process_mucsub(#iq{type = get, from = From, to = To} = IQ) ->
+process_mucsub(#iq{type = get, from = From, to = To,
+                  sub_els = [#muc_subscriptions{}]} = IQ) ->
     Host = To#jid.lserver,
     ServerHost = ejabberd_router:host_of_route(Host),
     RoomJIDs = get_subscribed_rooms(ServerHost, Host, From),
-    xmpp:make_iq_result(IQ, #muc_subscriptions{list = RoomJIDs}).
+    xmpp:make_iq_result(IQ, #muc_subscriptions{list = RoomJIDs});
+process_mucsub(#iq{lang = Lang} = IQ) ->
+    Txt = <<"No module is handling this query">>,
+    xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)).
 
 -spec is_create_request(stanza()) -> boolean().
 is_create_request(#presence{type = available}) ->
     true;
-is_create_request(#iq{type = set} = IQ) ->
-    xmpp:has_subtag(IQ, #muc_subscribe{});
-is_create_request(#iq{type = get} = IQ) ->
-    #muc_owner{} == xmpp:get_subtag(IQ, #muc_owner{});
+is_create_request(#iq{type = T} = IQ) when T == get; T == set ->
+    xmpp:has_subtag(IQ, #muc_subscribe{}) orelse
+    xmpp:has_subtag(IQ, #muc_owner{});
 is_create_request(_) ->
     false.
 
@@ -645,13 +660,12 @@ get_vh_rooms(_, _) ->
 %%                  index = NewIndex}}
 %%     end.
 
-get_subscribed_rooms(ServerHost, Host, From) ->
-    Rooms = get_rooms(ServerHost, Host),
+get_subscribed_rooms(_ServerHost, Host, From) ->
+    Rooms = get_vh_rooms(Host),
     lists:flatmap(
-      fun(#muc_room{name_host = {Name, _}, opts = Opts}) ->
-             Subscribers = proplists:get_value(subscribers, Opts, []),
-             case lists:keymember(From, 1, Subscribers) of
-                 true -> [jid:make(Name, Host, <<>>)];
+      fun(#muc_online_room{name_host = {Name, _}, pid = Pid}) ->
+             case gen_fsm:sync_send_all_state_event(Pid, {is_subscriber, From}) of
+                 true -> [jid:make(Name, Host)];
                  false -> []
              end;
         (_) ->
index 52401f835d3d5d67bffebd402cf4b4c1c8d5ebde..ce6851bc5da8728499916c5fbd78e567f7b49e2f 100644 (file)
 -type fsm_stop() :: {stop, normal, state()}.
 -type fsm_next() :: {next_state, normal_state, state()}.
 -type fsm_transition() :: fsm_stop() | fsm_next().
--type history_element() :: {binary(), %% nick
-                           message(), %% message itself
-                           boolean(), %% have subject
-                           erlang:timestamp(),
-                           non_neg_integer()}.
 
 -export_type([state/0]).
 
@@ -252,7 +247,8 @@ normal_state({route, From, <<"">>,
            ejabberd_router:route_error(StateData#state.jid, From, Packet, Err),
            {next_state, normal_state, StateData};
        false when Type /= error ->
-           handle_roommessage_from_nonparticipant(Packet, StateData, From);
+           handle_roommessage_from_nonparticipant(Packet, StateData, From),
+           {next_state, normal_state, StateData};
        false ->
            {next_state, normal_state, StateData}
     end;
@@ -279,9 +275,6 @@ normal_state({route, From, <<"">>,
                               process_iq_owner(From, IQ, StateData);
                           ?NS_DISCO_INFO when SubEl#disco_info.node == <<>> ->
                               process_iq_disco_info(From, IQ, StateData);
-                          ?NS_DISCO_INFO ->
-                              Txt = <<"Disco info is not available for this node">>,
-                              {error, xmpp:err_service_unavailable(Txt, Lang)};
                           ?NS_DISCO_ITEMS ->
                               process_iq_disco_items(From, IQ, StateData);
                           ?NS_VCARD ->
@@ -291,7 +284,9 @@ normal_state({route, From, <<"">>,
                           ?NS_CAPTCHA ->
                               process_iq_captcha(From, IQ, StateData);
                           _ ->
-                              {error, xmpp:err_feature_not_implemented()}
+                              Txt = <<"The feature requested is not "
+                                      "supported by the conference">>,
+                              {error, xmpp:err_service_unavailable(Txt, Lang)}
                       end,
                {IQRes, NewStateData} =
                    case Res1 of
@@ -312,8 +307,12 @@ normal_state({route, From, <<"">>,
                        ok
                end,
                case NewStateData of
-                   stop -> {stop, normal, StateData};
-                   _ -> {next_state, normal_state, NewStateData}
+                   stop ->
+                       {stop, normal, StateData};
+                   _ when NewStateData#state.just_created ->
+                       close_room_if_temporary_and_empty(NewStateData);
+                   _ ->
+                       {next_state, normal_state, NewStateData}
                end
        end
     catch _:{xmpp_codec, Why} ->
@@ -324,7 +323,10 @@ normal_state({route, From, <<"">>,
 normal_state({route, From, <<"">>, #iq{} = IQ}, StateData) ->
     Err = xmpp:err_bad_request(),
     ejabberd_router:route_error(StateData#state.jid, From, IQ, Err),
-    {next_state, normal_state, StateData};
+    case StateData#state.just_created of
+       true -> {stop, normal, StateData};
+       false -> {next_state, normal_state, StateData}
+    end;
 normal_state({route, From, Nick, #presence{} = Packet}, StateData) ->
     Activity = get_user_activity(From, StateData),
     Now = p1_time_compat:system_time(micro_seconds),
@@ -530,8 +532,14 @@ handle_sync_event({change_state, NewStateData}, _From,
                  StateName, _StateData) ->
     {reply, {ok, NewStateData}, StateName, NewStateData};
 handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) ->
-    NSD = process_item_change(Item, StateData, UJID),
-    {reply, {ok, NSD}, StateName, NSD};
+    case process_item_change(Item, StateData, UJID) of
+       {error, _} = Err ->
+           {reply, Err, StateName, StateData};
+       NSD ->
+           {reply, {ok, NSD}, StateName, NSD}
+    end;
+handle_sync_event({is_subscriber, From}, _From, StateName, StateData) ->
+    {reply, is_subscriber(From, StateData), StateName, StateData};
 handle_sync_event(_Event, _From, StateName,
                  StateData) ->
     Reply = ok, {reply, Reply, StateName, StateData}.
@@ -763,7 +771,7 @@ process_normal_message(From, #message{lang = Lang} = Pkt, StateData) ->
                  (#muc_user{invites = [I]}, _) ->
                       {ok, I};
                  (#muc_user{invites = [_|_]}, _) ->
-                      Txt = <<"Multiple <invite/> elements are not allowed">>,
+                      Txt = <<"Multiple invitations are not allowed">>,
                       {error, xmpp:err_resource_constraint(Txt, Lang)};
                  (#xdata{type = submit, fields = Fs}, _) ->
                       try {ok, muc_request:decode(Fs)}
@@ -835,7 +843,7 @@ process_voice_request(From, Pkt, StateData) ->
                {ok, _, _} ->
                    ErrText = <<"Please, wait for a while before sending "
                                "new voice request">>,
-                   Err = xmpp:err_not_acceptable(ErrText, Lang),
+                   Err = xmpp:err_resource_constraint(ErrText, Lang),
                    ejabberd_router:route_error(
                      StateData#state.jid, From, Pkt, Err),
                    StateData#state{last_voice_request_time = Times}
@@ -1147,10 +1155,9 @@ decide_fate_message(#message{type = error} = Msg,
     PD = case check_error_kick(Err) of
           %% If this is an error stanza and its condition matches a criteria
           true ->
-              Reason =
-                  io_lib:format("This participant is considered a ghost "
-                                "and is expulsed: ~s",
-                                [jid:to_string(From)]),
+              Reason = str:format("This participant is considered a ghost "
+                                  "and is expulsed: ~s",
+                                  [jid:to_string(From)]),
               {expulse_sender, Reason};
           false -> continue_delivery
         end,
@@ -1198,7 +1205,7 @@ get_error_condition(undefined) ->
 make_reason(Packet, From, StateData, Reason1) ->
     {ok, #user{nick = FromNick}} = (?DICT):find(jid:tolower(From), StateData#state.users),
     Condition = get_error_condition(xmpp:get_error(Packet)),
-    iolist_to_binary(io_lib:format(Reason1, [FromNick, Condition])).
+    str:format(Reason1, [FromNick, Condition]).
 
 -spec expulse_participant(stanza(), jid(), state(), binary()) ->
                                 state().
@@ -1816,11 +1823,9 @@ add_new_user(From, Nick, Packet, StateData) ->
                                                           Nodes, StateData)),
                              send_existing_presences(From, NewState),
                              send_initial_presence(From, NewState, StateData),
-                             Shift = count_stanza_shift(Nick, Packet, NewState),
-                             case send_history(From, Shift, NewState) of
-                                 true -> ok;
-                                 _ -> send_subject(From, StateData)
-                             end,
+                             History = get_history(Nick, Packet, NewState),
+                             send_history(From, History, NewState),
+                             send_subject(From, StateData),
                              NewState;
                         true ->
                              add_online_user(From, Nick, none,
@@ -1963,84 +1968,43 @@ extract_password(Packet) ->
            false
     end.
 
--spec count_stanza_shift(binary(), stanza(), state()) -> non_neg_integer().
-count_stanza_shift(Nick, Packet, StateData) ->
-    case xmpp:get_subtag(Packet, #muc_history{}) of
-       #muc_history{since = Since,
-                    seconds = Seconds,
-                    maxstanzas = MaxStanzas,
-                    maxchars = MaxChars} ->
-           HL = lqueue_to_list(StateData#state.history),
-           Shift0 = case Since of
-                        undefined -> 0;
-                        _ ->
-                            Sin = calendar:datetime_to_gregorian_seconds(
-                                    calendar:now_to_datetime(Since)),
-                            count_seconds_shift(Sin, HL)
-                    end,
-           Shift1 = case Seconds of
-                        undefined -> 0;
-                        _ ->
-                            Sec = calendar:datetime_to_gregorian_seconds(
-                                    calendar:universal_time()) - Seconds,
-                            count_seconds_shift(Sec, HL)
-                    end,
-           Shift2 = case MaxStanzas of
-                        undefined -> 0;
-                        _ -> count_maxstanzas_shift(MaxStanzas, HL)
-                    end,
-           Shift3 = case MaxChars of
-                        undefined -> 0;
-                        _ -> count_maxchars_shift(Nick, MaxChars, HL)
-                    end,
-           lists:max([Shift0, Shift1, Shift2, Shift3]);
-       false ->
-           0
-    end.
-
--spec count_seconds_shift(non_neg_integer(),
-                         [history_element()]) -> non_neg_integer().
-count_seconds_shift(Seconds, HistoryList) ->
-    lists:sum(lists:map(fun ({_Nick, _Packet, _HaveSubject,
-                             TimeStamp, _Size}) ->
-                               T =
-                                   calendar:datetime_to_gregorian_seconds(TimeStamp),
-                               if T < Seconds -> 1;
-                                  true -> 0
-                               end
-                       end,
-                       HistoryList)).
-
--spec count_maxstanzas_shift(non_neg_integer(),
-                            [history_element()]) -> non_neg_integer().
-count_maxstanzas_shift(MaxStanzas, HistoryList) ->
-    S = length(HistoryList) - MaxStanzas,
-    if S =< 0 -> 0;
-       true -> S
-    end.
-
--spec count_maxchars_shift(binary(), non_neg_integer(),
-                          [history_element()]) -> integer().
-count_maxchars_shift(Nick, MaxSize, HistoryList) ->
-    NLen = byte_size(Nick) + 1,
-    Sizes = lists:map(fun ({_Nick, _Packet, _HaveSubject,
-                           _TimeStamp, Size}) ->
-                             Size + NLen
-                     end,
-                     HistoryList),
-    calc_shift(MaxSize, Sizes).
-
--spec calc_shift(non_neg_integer(), [non_neg_integer()]) -> integer().
-calc_shift(MaxSize, Sizes) ->
-    Total = lists:sum(Sizes),
-    calc_shift(MaxSize, Total, 0, Sizes).
-
--spec calc_shift(non_neg_integer(), integer(), integer(),
-                [non_neg_integer()]) -> integer().
-calc_shift(_MaxSize, _Size, Shift, []) -> Shift;
-calc_shift(MaxSize, Size, Shift, [S | TSizes]) ->
-    if MaxSize >= Size -> Shift;
-       true -> calc_shift(MaxSize, Size - S, Shift + 1, TSizes)
+-spec get_history(binary(), stanza(), state()) -> lqueue().
+get_history(Nick, Packet, #state{history = History}) ->
+    case xmpp:get_subtag(Packet, #muc{}) of
+       #muc{history = #muc_history{} = MUCHistory} ->
+           Now = p1_time_compat:timestamp(),
+           Q = History#lqueue.queue,
+           {NewQ, Len} = filter_history(Q, MUCHistory, Now, Nick, queue:new(), 0, 0),
+           History#lqueue{queue = NewQ, len = Len};
+       _ ->
+           History
+    end.
+
+-spec filter_history(?TQUEUE, muc_history(), erlang:timestamp(), binary(),
+                    ?TQUEUE, non_neg_integer(), non_neg_integer()) ->
+                           {?TQUEUE, non_neg_integer()}.
+filter_history(Queue, #muc_history{since = Since,
+                                  seconds = Seconds,
+                                  maxstanzas = MaxStanzas,
+                                  maxchars = MaxChars} = MUC,
+              Now, Nick, AccQueue, NumStanzas, NumChars) ->
+    case queue:out_r(Queue) of
+       {{value, {_, _, _, TimeStamp, Size} = Elem}, NewQueue} ->
+           NowDiff = timer:now_diff(Now, TimeStamp) div 1000000,
+           Chars = Size + byte_size(Nick) + 1,
+           if (NumStanzas < MaxStanzas) andalso
+              (TimeStamp > Since) andalso
+              (NowDiff =< Seconds) andalso
+              (NumChars + Chars =< MaxChars) ->
+                   filter_history(NewQueue, MUC, Now, Nick,
+                                  queue:in_r(Elem, AccQueue),
+                                  NumStanzas + 1,
+                                  NumChars + Chars);
+              true ->
+                   {AccQueue, NumStanzas}
+           end;
+       {empty, _} ->
+           {AccQueue, NumStanzas}
     end.
 
 -spec is_room_overcrowded(state()) -> boolean().
@@ -2166,19 +2130,20 @@ send_new_presence1(NJID, Reason, IsInitialPresence, StateData, OldStateData) ->
         end,
     lists:foreach(
       fun({LUJID, Info}) ->
-             {Role, Presence} = if LNJID == LUJID -> {Role0, Presence0};
+             IsSelfPresence = LNJID == LUJID,
+             {Role, Presence} = if IsSelfPresence -> {Role0, Presence0};
                                    true -> {Role1, Presence1}
                                 end,
              Item0 = #muc_item{affiliation = Affiliation,
                                role = Role},
              Item1 = case Info#user.role == moderator orelse
                          (StateData#state.config)#config.anonymous
-                         == false of
+                         == false orelse IsSelfPresence of
                          true -> Item0#muc_item{jid = RealJID};
                          false -> Item0
                      end,
              Item = Item1#muc_item{reason = Reason},
-             StatusCodes = status_codes(IsInitialPresence, NJID, Info,
+             StatusCodes = status_codes(IsInitialPresence, IsSelfPresence,
                                         StateData),
              Pres = if Presence == undefined -> #presence{};
                        true -> Presence
@@ -2303,30 +2268,26 @@ send_nick_changing(JID, OldNick, StateData,
                     StateData#state.users),
     Affiliation = get_affiliation(JID, StateData),
     lists:foreach(
-      fun({_LJID, Info}) when Presence /= undefined ->
+      fun({LJID, Info}) when Presence /= undefined ->
+             IsSelfPresence = LJID == jid:tolower(JID),
              Item0 = #muc_item{affiliation = Affiliation, role = Role},
-             Item1 = case Info#user.role == moderator orelse
-                         (StateData#state.config)#config.anonymous
-                         == false of
-                         true -> Item0#muc_item{jid = RealJID, nick = Nick};
-                         false -> Item0#muc_item{nick = Nick}
-                     end,
-             Item2 = case Info#user.role == moderator orelse
-                         (StateData#state.config)#config.anonymous
-                         == false of
-                         true -> Item0#muc_item{jid = RealJID};
-                         false -> Item0
-                     end,
-             Status110 = case JID == Info#user.jid of
+             Item = case Info#user.role == moderator orelse
+                        (StateData#state.config)#config.anonymous
+                        == false orelse IsSelfPresence of
+                        true -> Item0#muc_item{jid = RealJID};
+                        false -> Item0
+                    end,
+             Status110 = case IsSelfPresence of
                              true -> [110];
                              false -> []
                          end,
-             Packet1 = #presence{type = unavailable,
-                                 sub_els = [#muc_user{
-                                               items = [Item1],
-                                               status_codes = [303|Status110]}]},
+             Packet1 = #presence{
+                          type = unavailable,
+                          sub_els = [#muc_user{
+                                        items = [Item#muc_item{nick = Nick}],
+                                        status_codes = [303|Status110]}]},
              Packet2 = xmpp:set_subtag(Presence,
-                                       #muc_user{items = [Item2],
+                                       #muc_user{items = [Item],
                                                  status_codes = Status110}),
              if SendOldUnavailable ->
                      send_wrapped(
@@ -2364,12 +2325,12 @@ maybe_send_affiliation(JID, Affiliation, StateData) ->
       true ->
          ok; % The new affiliation is published via presence.
       false ->
-         send_affiliation(LJID, Affiliation, StateData)
+         send_affiliation(JID, Affiliation, StateData)
     end.
 
--spec send_affiliation(ljid(), affiliation(), state()) -> ok.
-send_affiliation(LJID, Affiliation, StateData) ->
-    Item = #muc_item{jid = jid:make(LJID),
+-spec send_affiliation(jid(), affiliation(), state()) -> ok.
+send_affiliation(JID, Affiliation, StateData) ->
+    Item = #muc_item{jid = JID,
                     affiliation = Affiliation,
                     role = none},
     Message = #message{id = randoms:get_string(),
@@ -2388,8 +2349,8 @@ send_affiliation(LJID, Affiliation, StateData) ->
                  StateData#state.server_host,
                  Recipients, Message).
 
--spec status_codes(boolean(), jid(), #user{}, state()) -> [pos_integer()].
-status_codes(IsInitialPresence, JID, #user{jid = JID}, StateData) ->
+-spec status_codes(boolean(), boolean(), state()) -> [pos_integer()].
+status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) ->
     S0 = [110],
     case IsInitialPresence of
        true ->
@@ -2408,7 +2369,7 @@ status_codes(IsInitialPresence, JID, #user{jid = JID}, StateData) ->
            S3;
        false -> S0
     end;
-status_codes(_IsInitialPresence, _JID, _Info, _StateData) -> [].
+status_codes(_IsInitialPresence, _IsSelfPresence = false, _StateData) -> [].
 
 -spec lqueue_new(non_neg_integer()) -> lqueue().
 lqueue_new(Max) ->
@@ -2438,54 +2399,58 @@ lqueue_to_list(#lqueue{queue = Q1}) ->
 
 -spec add_message_to_history(binary(), jid(), message(), state()) -> state().
 add_message_to_history(FromNick, FromJID, Packet, StateData) ->
-    HaveSubject = Packet#message.subject /= [],
-    TimeStamp = p1_time_compat:timestamp(),
-    AddrPacket = case (StateData#state.config)#config.anonymous of
-                  true -> Packet;
-                  false ->
-                      Addresses = #addresses{
-                                     list = [#address{type = ofrom,
-                                                      jid = FromJID}]},
-                      xmpp:set_subtag(Packet, Addresses)
-                end,
-    TSPacket = xmpp_util:add_delay_info(
-                AddrPacket, StateData#state.jid, TimeStamp),
-    SPacket = xmpp:set_from_to(
-               TSPacket,
-               jid:replace_resource(StateData#state.jid, FromNick),
-               StateData#state.jid),
-    Size = element_size(SPacket),
-    Q1 = lqueue_in({FromNick, TSPacket, HaveSubject,
-                   calendar:now_to_universal_time(TimeStamp), Size},
-                  StateData#state.history),
     add_to_log(text, {FromNick, Packet}, StateData),
-    StateData#state{history = Q1}.
-
--spec send_history(jid(), integer(), state()) -> boolean().
-send_history(JID, Shift, StateData) ->
-    lists:foldl(fun ({Nick, Packet, HaveSubject, _TimeStamp,
-                     _Size},
-                    B) ->
-                       ejabberd_router:route(jid:replace_resource(StateData#state.jid,
-                                                              Nick),
-                                    JID, Packet),
-                       B or HaveSubject
-               end,
-               false,
-               lists:nthtail(Shift,
-                             lqueue_to_list(StateData#state.history))).
+    case check_subject(Packet) of
+       false ->
+           TimeStamp = p1_time_compat:timestamp(),
+           AddrPacket = case (StateData#state.config)#config.anonymous of
+                            true -> Packet;
+                            false ->
+                                Addresses = #addresses{
+                                               list = [#address{type = ofrom,
+                                                                jid = FromJID}]},
+                                xmpp:set_subtag(Packet, Addresses)
+                        end,
+           TSPacket = xmpp_util:add_delay_info(
+                        AddrPacket, StateData#state.jid, TimeStamp),
+           SPacket = xmpp:set_from_to(
+                       TSPacket,
+                       jid:replace_resource(StateData#state.jid, FromNick),
+                       StateData#state.jid),
+           Size = element_size(SPacket),
+           Q1 = lqueue_in({FromNick, TSPacket, false,
+                           TimeStamp, Size},
+                          StateData#state.history),
+           StateData#state{history = Q1};
+       _ ->
+           StateData
+    end.
+
+-spec send_history(jid(), lqueue(), state()) -> boolean().
+send_history(JID, History, StateData) ->
+    lists:foreach(
+      fun({Nick, Packet, _HaveSubject, _TimeStamp, _Size}) ->
+             ejabberd_router:route(
+               jid:replace_resource(StateData#state.jid, Nick),
+               JID, Packet)
+      end, lqueue_to_list(History)).
 
 -spec send_subject(jid(), state()) -> ok.
-send_subject(_JID, #state{subject_author = <<"">>}) -> ok;
 send_subject(JID, #state{subject_author = Nick} = StateData) ->
-    Subject = StateData#state.subject,
-    Packet = #message{type = groupchat, subject = xmpp:mk_text(Subject)},
+    Subject = case StateData#state.subject of
+                 <<"">> -> [#text{}];
+                 Subj -> xmpp:mk_text(Subj)
+             end,
+    Packet = #message{type = groupchat, subject = Subject},
     ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), JID,
                          Packet).
 
 -spec check_subject(message()) -> false | binary().
-check_subject(#message{subject = []}) -> false;
-check_subject(#message{subject = Subj}) -> xmpp:get_text(Subj).
+check_subject(#message{subject = [_|_] = Subj, body = [],
+                      thread = undefined}) ->
+    xmpp:get_text(Subj);
+check_subject(_) ->
+    false.
 
 -spec can_change_subject(role(), state()) -> boolean().
 can_change_subject(Role, StateData) ->
@@ -2552,10 +2517,10 @@ items_with_role(SRole, StateData) ->
 items_with_affiliation(SAffiliation, StateData) ->
     lists:map(
       fun({JID, {Affiliation, Reason}}) ->
-             #muc_item{affiliation = Affiliation, jid = JID,
+             #muc_item{affiliation = Affiliation, jid = jid:make(JID),
                        reason = Reason};
         ({JID, Affiliation}) ->
-             #muc_item{affiliation = Affiliation, jid = JID}
+             #muc_item{affiliation = Affiliation, jid = jid:make(JID)}
       end,
       search_affiliation(SAffiliation, StateData)).
 
@@ -2600,23 +2565,29 @@ process_admin_items_set(UJID, Items, Lang, StateData) ->
                    "room ~s:~n ~p",
                    [jid:to_string(UJID),
                     jid:to_string(StateData#state.jid), Res]),
-         NSD = lists:foldl(process_item_change(UJID),
-                           StateData, lists:flatten(Res)),
-         store_room(NSD),
-         {result, undefined, NSD};
-      {error, Err} -> {error, Err}
+         case lists:foldl(process_item_change(UJID),
+                          StateData, lists:flatten(Res)) of
+             {error, _} = Err ->
+                 Err;
+             NSD ->
+                 store_room(NSD),
+                 {result, undefined, NSD}
+         end;
+       {error, Err} -> {error, Err}
     end.
 
 -spec process_item_change(jid()) -> function().
 process_item_change(UJID) ->
-    fun(E, SD) ->
-        process_item_change(E, SD, UJID)
+    fun(_, {error, _} = Err) ->
+           Err;
+       (Item, SD) ->
+           process_item_change(Item, SD, UJID)
     end.
 
 -type admin_action() :: {jid(), affiliation | role,
                         affiliation() | role(), binary()}.
 
--spec process_item_change(admin_action(), state(), jid()) -> state().
+-spec process_item_change(admin_action(), state(), jid()) -> state() | {error, stanza_error()}.
 process_item_change(Item, SD, UJID) ->
     try case Item of
            {JID, affiliation, owner, _} when JID#jid.luser == <<"">> ->
@@ -2624,23 +2595,23 @@ process_item_change(Item, SD, UJID) ->
                %% forget the affiliation completely
                SD;
            {JID, role, none, Reason} ->
-               catch send_kickban_presence(UJID, JID, Reason, 307, SD),
+               send_kickban_presence(UJID, JID, Reason, 307, SD),
                set_role(JID, none, SD);
            {JID, affiliation, none, Reason} ->
                case (SD#state.config)#config.members_only of
                    true ->
-                       catch send_kickban_presence(UJID, JID, Reason, 321, none, SD),
+                       send_kickban_presence(UJID, JID, Reason, 321, none, SD),
                        maybe_send_affiliation(JID, none, SD),
                        SD1 = set_affiliation(JID, none, SD),
                        set_role(JID, none, SD1);
                    _ ->
                        SD1 = set_affiliation(JID, none, SD),
-                       send_update_presence(JID, SD1, SD),
+                       send_update_presence(JID, Reason, SD1, SD),
                        maybe_send_affiliation(JID, none, SD1),
                        SD1
                end;
            {JID, affiliation, outcast, Reason} ->
-               catch send_kickban_presence(UJID, JID, Reason, 301, outcast, SD),
+               send_kickban_presence(UJID, JID, Reason, 301, outcast, SD),
                maybe_send_affiliation(JID, outcast, SD),
                set_affiliation(JID, outcast, set_role(JID, none, SD), Reason);
            {JID, affiliation, A, Reason} when (A == admin) or (A == owner) ->
@@ -2657,7 +2628,7 @@ process_item_change(Item, SD, UJID) ->
                SD2;
            {JID, role, Role, Reason} ->
                SD1 = set_role(JID, Role, SD),
-               catch send_new_presence(JID, Reason, SD1, SD),
+               send_new_presence(JID, Reason, SD1, SD),
                SD1;
            {JID, affiliation, A, _Reason} ->
                SD1 = set_affiliation(JID, A, SD),
@@ -2669,7 +2640,7 @@ process_item_change(Item, SD, UJID) ->
            ?ERROR_MSG("failed to set item ~p from ~s: ~p",
                       [Item, jid:to_string(UJID),
                        {E, {R, erlang:get_stacktrace()}}]),
-           SD
+           {error, xmpp:err_internal_server_error()}
     end.
 
 -spec find_changed_items(jid(), affiliation(), role(),
@@ -2698,12 +2669,11 @@ find_changed_items(UJID, UAffiliation, URole,
           Nick /= <<"">> ->
                case find_jids_by_nick(Nick, StateData) of
                    [] ->
-                       ErrText = iolist_to_binary(
-                                   io_lib:format(
-                                     translate:translate(
-                                       Lang,
-                                       <<"Nickname ~s does not exist in the room">>),
-                                     [Nick])),
+                       ErrText = str:format(
+                                   translate:translate(
+                                     Lang,
+                                     <<"Nickname ~s does not exist in the room">>),
+                                   [Nick]),
                        throw({error, xmpp:err_not_acceptable(ErrText, Lang)});
                    JIDList ->
                        JIDList
@@ -2740,9 +2710,15 @@ find_changed_items(UJID, UAffiliation, URole,
                               Items, Lang, StateData,
                               Res);
        true ->
-           MoreRes = [{jid:remove_resource(Jidx),
-                       RoleOrAff, RoleOrAffValue, Reason}
-                      || Jidx <- JIDs],
+           MoreRes = case RoleOrAff of
+                         affiliation ->
+                             [{jid:remove_resource(Jidx),
+                               RoleOrAff, RoleOrAffValue, Reason}
+                              || Jidx <- JIDs];
+                         role ->
+                             [{Jidx, RoleOrAff, RoleOrAffValue, Reason}
+                              || Jidx <- JIDs]
+                     end,
            find_changed_items(UJID, UAffiliation, URole,
                               Items, Lang, StateData,
                               [MoreRes | Res]);
@@ -2925,12 +2901,13 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
                     StateData#state.users),
     ActorNick = get_actor_nick(MJID, StateData),
     lists:foreach(
-      fun({_LJID, Info}) ->
+      fun({LJID, Info}) ->
+             IsSelfPresence = jid:tolower(UJID) == LJID,
              Item0 = #muc_item{affiliation = Affiliation,
                                role = none},
              Item1 = case Info#user.role == moderator orelse
                          (StateData#state.config)#config.anonymous
-                         == false of
+                         == false orelse IsSelfPresence of
                          true -> Item0#muc_item{jid = RealJID};
                          false -> Item0
                      end,
@@ -2939,9 +2916,12 @@ send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation,
                         <<"">> -> Item2;
                         _ -> Item2#muc_item{actor = #muc_actor{nick = ActorNick}}
                     end,
+             Codes = if IsSelfPresence -> [110, Code];
+                        true -> [Code]
+                     end,
              Packet = #presence{type = unavailable,
                                 sub_els = [#muc_user{items = [Item],
-                                                     status_codes = [Code]}]},
+                                                     status_codes = Codes}]},
              RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick),
              send_wrapped(RoomJIDNick, Info#user.jid, Packet,
                           ?NS_MUCSUB_NODES_AFFILIATIONS, StateData),
@@ -2989,13 +2969,21 @@ process_iq_owner(From, #iq{type = set, lang = Lang,
            case Config of
                #xdata{type = cancel} ->
                    {result, undefined};
-               #xdata{type = submit} ->
-                   case is_allowed_log_change(Config, StateData, From) andalso
-                       is_allowed_persistent_change(Config, StateData, From) andalso
-                       is_allowed_room_name_desc_limits(Config, StateData) andalso
-                       is_password_settings_correct(Config, StateData) of
-                       true -> set_config(Config, StateData, Lang);
-                       false -> {error, xmpp:err_not_acceptable()}
+               #xdata{type = submit, fields = Fs} ->
+                   try muc_roomconfig:decode(Fs) of
+                       Options ->
+                           case is_allowed_log_change(Options, StateData, From) andalso
+                               is_allowed_persistent_change(Options, StateData, From) andalso
+                               is_allowed_room_name_desc_limits(Options, StateData) andalso
+                               is_password_settings_correct(Options, StateData) of
+                               true ->
+                                   set_config(Options, StateData, Lang);
+                               false ->
+                                   {error, xmpp:err_not_acceptable()}
+                           end
+                   catch _:{muc_roomconfig, Why} ->
+                           Txt = muc_roomconfig:format_error(Why),
+                           {error, xmpp:err_bad_request(Txt, Lang)}
                    end;
                _ ->
                    Txt = <<"Incorrect data form">>,
@@ -3034,9 +3022,9 @@ process_iq_owner(From, #iq{type = get, lang = Lang,
            {error, xmpp:err_bad_request()}
     end.
 
--spec is_allowed_log_change(xdata(), state(), jid()) -> boolean().
-is_allowed_log_change(X, StateData, From) ->
-    case xmpp_util:has_xdata_var(<<"muc#roomconfig_enablelogging">>, X) of
+-spec is_allowed_log_change(muc_roomconfig:result(), state(), jid()) -> boolean().
+is_allowed_log_change(Options, StateData, From) ->
+    case proplists:is_defined(enablelogging, Options) of
        false -> true;
        true ->
            allow ==
@@ -3044,9 +3032,9 @@ is_allowed_log_change(X, StateData, From) ->
                                             From)
     end.
 
--spec is_allowed_persistent_change(xdata(), state(), jid()) -> boolean().
-is_allowed_persistent_change(X, StateData, From) ->
-    case xmpp_util:has_xdata_var(<<"muc#roomconfig_persistentroom">>, X) of
+-spec is_allowed_persistent_change(muc_roomconfig:result(), state(), jid()) -> boolean().
+is_allowed_persistent_change(Options, StateData, From) ->
+    case proplists:is_defined(persistentroom, Options) of
       false -> true;
       true ->
          {_AccessRoute, _AccessCreate, _AccessAdmin,
@@ -3059,66 +3047,43 @@ is_allowed_persistent_change(X, StateData, From) ->
 
 %% Check if the Room Name and Room Description defined in the Data Form
 %% are conformant to the configured limits
--spec is_allowed_room_name_desc_limits(xdata(), state()) -> boolean().
-is_allowed_room_name_desc_limits(XData, StateData) ->
-    IsNameAccepted = case xmpp_util:get_xdata_values(
-                           <<"muc#roomconfig_roomname">>, XData) of
-                        [N] ->
-                            byte_size(N) =<
-                                gen_mod:get_module_opt(
-                                  StateData#state.server_host,
-                                  mod_muc, max_room_name,
-                                  fun(infinity) -> infinity;
-                                     (I) when is_integer(I),
-                                              I>0 -> I
-                                  end, infinity);
-                        _ ->
-                            true
-                    end,
-    IsDescAccepted = case xmpp_util:get_xdata_values(
-                           <<"muc#roomconfig_roomdesc">>, XData) of
-                        [D] ->
-                            byte_size(D) =<
-                                gen_mod:get_module_opt(
-                                  StateData#state.server_host,
-                                  mod_muc, max_room_desc,
-                                  fun(infinity) -> infinity;
-                                     (I) when is_integer(I),
-                                              I>0 ->
-                                          I
-                                  end, infinity);
-                        _ -> true
-                    end,
-    IsNameAccepted and IsDescAccepted.
+-spec is_allowed_room_name_desc_limits(muc_roomconfig:result(), state()) -> boolean().
+is_allowed_room_name_desc_limits(Options, StateData) ->
+    RoomName = proplists:get_value(roomname, Options, <<"">>),
+    RoomDesc = proplists:get_value(roomdesc, Options, <<"">>),
+    MaxRoomName = gen_mod:get_module_opt(
+                   StateData#state.server_host,
+                   mod_muc, max_room_name,
+                   fun(infinity) -> infinity;
+                      (I) when is_integer(I),
+                               I>0 -> I
+                   end, infinity),
+    MaxRoomDesc = gen_mod:get_module_opt(
+                   StateData#state.server_host,
+                   mod_muc, max_room_desc,
+                   fun(infinity) -> infinity;
+                      (I) when is_integer(I),
+                               I>0 ->
+                           I
+                   end, infinity),
+    (byte_size(RoomName) =< MaxRoomName)
+       andalso (byte_size(RoomDesc) =< MaxRoomDesc).
 
 %% Return false if:
 %% "the password for a password-protected room is blank"
--spec is_password_settings_correct(xdata(), state()) -> boolean().
-is_password_settings_correct(XData, StateData) ->
+-spec is_password_settings_correct(muc_roomconfig:result(), state()) -> boolean().
+is_password_settings_correct(Options, StateData) ->
     Config = StateData#state.config,
     OldProtected = Config#config.password_protected,
     OldPassword = Config#config.password,
-    NewProtected = case xmpp_util:get_xdata_values(
-                         <<"muc#roomconfig_passwordprotectedroom">>, XData) of
-                      [<<"1">>] -> true;
-                      [<<"true">>] -> true;
-                      [<<"0">>] -> false;
-                      [<<"false">>] -> false;
-                      _ -> undefined
-                  end,
-    NewPassword = case xmpp_util:get_xdata_values(
-                        <<"muc#roomconfig_roomsecret">>, XData) of
-                     [P] -> P;
-                     _ -> undefined
-                 end,
-    case {OldProtected, NewProtected, OldPassword,
-         NewPassword}
-       of
-      {true, undefined, <<"">>, undefined} -> false;
-      {true, undefined, _, <<"">>} -> false;
-      {_, true, <<"">>, undefined} -> false;
-      {_, true, _, <<"">>} -> false;
-      _ -> true
+    NewProtected = proplists:get_value(passwordprotectedroom, Options),
+    NewPassword = proplists:get_value(roomsecret, Options),
+    case {OldProtected, NewProtected, OldPassword, NewPassword} of
+       {true, undefined, <<"">>, undefined} -> false;
+       {true, undefined, _, <<"">>} -> false;
+       {_, true, <<"">>, undefined} -> false;
+       {_, true, _, <<"">>} -> false;
+       _ -> true
     end.
 
 -spec get_default_room_maxusers(state()) -> non_neg_integer().
@@ -3144,10 +3109,9 @@ get_config(Lang, StateData, From) ->
                {N, N};
            _ -> {0, none}
        end,
-    Title = iolist_to_binary(
-             io_lib:format(
-               translate:translate(Lang, <<"Configuration of room ~s">>),
-               [jid:to_string(StateData#state.jid)])),
+    Title = str:format(
+             translate:translate(Lang, <<"Configuration of room ~s">>),
+             [jid:to_string(StateData#state.jid)]),
     Fs = [{roomname, Config#config.title},
          {roomdesc, Config#config.description}] ++
        case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of
@@ -3208,11 +3172,10 @@ get_config(Lang, StateData, From) ->
           fields = muc_roomconfig:encode(
                      Fields, fun(T) -> translate:translate(Lang, T) end)}.
 
--spec set_config(xdata(), state(), binary()) -> {error, stanza_error()} |
-                                               {result, undefined, state()}.
-set_config(#xdata{fields = Fields}, StateData, Lang) ->
+-spec set_config(muc_roomconfig:result(), state(), binary()) ->
+                       {error, stanza_error()} | {result, undefined, state()}.
+set_config(Options, StateData, Lang) ->
     try
-       Options = muc_roomconfig:decode(Fields),
        #config{} = Config = set_config(Options, StateData#state.config,
                                        StateData#state.server_host, Lang),
        {result, _, NSD} = Res = change_config(Config, StateData),
@@ -3227,10 +3190,7 @@ set_config(#xdata{fields = Fields}, StateData, Lang) ->
                 || {_, U} <- (?DICT):to_list(StateData#state.users)],
        add_to_log(Type, Users, NSD),
        Res
-    catch _:{muc_roomconfig, Why} ->
-           Txt = muc_roomconfig:format_error(Why),
-           {error, xmpp:err_bad_request(Txt, Lang)};
-         _:{badmatch, {error, #stanza_error{}} = Err} ->
+    catch  _:{badmatch, {error, #stanza_error{}} = Err} ->
            Err
     end.
 
@@ -3331,18 +3291,23 @@ send_config_change_info(New, #state{config = Old} = StateData) ->
              end
                ++
                case Old#config{anonymous = New#config.anonymous,
+                               vcard = New#config.vcard,
                                logging = New#config.logging} of
                  New -> [];
                  _ -> [104]
                end,
-    Message = #message{type = groupchat,
-                      id = randoms:get_string(),
-                      sub_els = [#muc_user{status_codes = Codes}]},
-    send_wrapped_multiple(StateData#state.jid,
-                         StateData#state.users,
-                         Message,
-                         ?NS_MUCSUB_NODES_CONFIG,
-                         StateData).
+    if Codes /= [] ->
+           Message = #message{type = groupchat,
+                              id = randoms:get_string(),
+                              sub_els = [#muc_user{status_codes = Codes}]},
+           send_wrapped_multiple(StateData#state.jid,
+                                 StateData#state.users,
+                                 Message,
+                                 ?NS_MUCSUB_NODES_CONFIG,
+                                 StateData);
+       true ->
+           ok
+    end.
 
 -spec remove_nonmembers(state()) -> state().
 remove_nonmembers(StateData) ->
@@ -3620,7 +3585,7 @@ iq_disco_info_extras(Lang, StateData) ->
 process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) ->
     Txt = <<"Value 'set' of 'type' attribute is not allowed">>,
     {error, xmpp:err_not_allowed(Txt, Lang)};
-process_iq_disco_items(From, #iq{type = get, lang = Lang}, StateData) ->
+process_iq_disco_items(From, #iq{type = get}, StateData) ->
     case (StateData#state.config)#config.public_list of
       true ->
          {result, get_mucroom_disco_items(StateData)};
@@ -3629,8 +3594,10 @@ process_iq_disco_items(From, #iq{type = get, lang = Lang}, StateData) ->
            true ->
                {result, get_mucroom_disco_items(StateData)};
            _ ->
-               Txt = <<"Only occupants or administrators can perform this query">>,
-               {error, xmpp:err_forbidden(Txt, Lang)}
+               %% If the list of occupants is private,
+               %% the room MUST return an empty <query/> element
+               %% (http://xmpp.org/extensions/xep-0045.html#disco-roomitems)
+               {result, #disco_items{}}
          end
     end.
 
@@ -3661,7 +3628,7 @@ process_iq_vcard(_From, #iq{type = get}, StateData) ->
        #xmlel{} = VCard ->
            {result, VCard};
        {error, _} ->
-           {result, #vcard_temp{}}
+           {error, xmpp:err_item_not_found()}
     end;
 process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [SubEl]},
                 StateData) ->
@@ -3801,8 +3768,7 @@ get_roomdesc_tail(StateData, Lang) ->
             _ -> translate:translate(Lang, <<"private, ">>)
           end,
     Len = (?DICT):size(StateData#state.users),
-    <<" (", Desc/binary,
-      (iolist_to_binary(integer_to_list(Len)))/binary, ")">>.
+    <<" (", Desc/binary, (integer_to_binary(Len))/binary, ")">>.
 
 -spec get_mucroom_disco_items(state()) -> disco_items().
 get_mucroom_disco_items(StateData) ->
index 27d21075a4a939147976446019afc95f9b842382..439ae6a7ac7a866a10e2ce14be07dcb28c4442a2 100644 (file)
@@ -64,6 +64,7 @@
          to_float/1,
          prefix/2,
          suffix/2,
+        format/2,
          to_integer/1]).
 
 %%%===================================================================
@@ -277,6 +278,11 @@ prefix(Prefix, B) ->
 suffix(B1, B2) ->
     lists:suffix(binary_to_list(B1), binary_to_list(B2)).
 
+-spec format(io:format(), list()) -> binary().
+
+format(Format, Args) ->
+    iolist_to_binary(io_lib:format(Format, Args)).
+
 %%%===================================================================
 %%% Internal functions
 %%%===================================================================
index b4249bbdf7455723aa737d57abbbfe2941b4ce28..1a5c890768bfb00f5dce0b0bda8d322036174755 100644 (file)
 
 -compile(export_all).
 
--import(suite, [init_config/1, connect/1, disconnect/1,
-                recv/1, send/2, send_recv/2, my_jid/1, server_jid/1,
-                pubsub_jid/1, proxy_jid/1, muc_jid/1, muc_room_jid/1,
-               mix_jid/1, mix_room_jid/1, get_features/2, re_register/1,
-                is_feature_advertised/2, subscribe_to_events/1,
+-import(suite, [init_config/1, connect/1, disconnect/1, recv_message/1,
+                recv/1, recv_presence/1, send/2, send_recv/2, my_jid/1,
+               server_jid/1, pubsub_jid/1, proxy_jid/1, muc_jid/1,
+               muc_room_jid/1, my_muc_jid/1, peer_muc_jid/1,
+               mix_jid/1, mix_room_jid/1, get_features/2, recv_iq/1,
+               re_register/1, is_feature_advertised/2, subscribe_to_events/1,
                 is_feature_advertised/3, set_opt/3, auth_SASL/2,
-                wait_for_master/1, wait_for_slave/1,
-                make_iq_result/1, start_event_relay/0,
+                wait_for_master/1, wait_for_slave/1, flush/1,
+                make_iq_result/1, start_event_relay/0, alt_room_jid/1,
                 stop_event_relay/1, put_event/2, get_event/1,
                 bind/1, auth/1, auth/2, open_session/1, open_session/2,
                zlib/1, starttls/1, starttls/2, close_socket/1, init_stream/1,
                auth_legacy/2, auth_legacy/3, tcp_connect/1, send_text/2]).
-
 -include("suite.hrl").
 
 suite() ->
-    [{timetrap, {seconds,60}}].
+    [{timetrap, {seconds, 30}}].
 
 init_per_suite(Config) ->
     NewConfig = init_config(Config),
@@ -83,7 +83,7 @@ init_per_group(Group, Config) ->
 
 do_init_per_group(no_db, Config) ->
     re_register(Config),
-    Config;
+    set_opt(persistent_room, false, Config);
 do_init_per_group(mnesia, Config) ->
     mod_muc:shutdown_rooms(?MNESIA_VHOST),
     set_opt(server, ?MNESIA_VHOST, Config);
@@ -202,6 +202,10 @@ init_per_testcase(TestCase, OrigConfig) ->
     Test = atom_to_list(TestCase),
     IsMaster = lists:suffix("_master", Test),
     IsSlave = lists:suffix("_slave", Test),
+    Mode = if IsSlave -> slave;
+             IsMaster -> master;
+             true -> single
+          end,
     IsCarbons = lists:prefix("carbons_", Test),
     IsReplaced = lists:prefix("replaced_", Test),
     User = if IsReplaced -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>;
@@ -209,6 +213,10 @@ init_per_testcase(TestCase, OrigConfig) ->
               IsSlave -> <<"test_slave!#$%^*()`~+-;_=[]{}|\\">>;
               true -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>
            end,
+    Nick = if IsSlave -> ?config(slave_nick, OrigConfig);
+             IsMaster -> ?config(master_nick, OrigConfig);
+             true -> ?config(nick, OrigConfig)
+          end,
     MyResource = if IsMaster and IsCarbons -> MasterResource;
                    IsSlave and IsCarbons -> SlaveResource;
                    true -> Resource
@@ -227,10 +235,23 @@ init_per_testcase(TestCase, OrigConfig) ->
                true ->
                     jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource)
             end,
-    Config = set_opt(user, User,
-                     set_opt(slave, Slave,
-                             set_opt(master, Master,
-                                    set_opt(resource, MyResource, OrigConfig)))),
+    Config1 = set_opt(user, User,
+                     set_opt(slave, Slave,
+                             set_opt(master, Master,
+                                     set_opt(resource, MyResource,
+                                             set_opt(nick, Nick,
+                                                     set_opt(mode, Mode, OrigConfig)))))),
+    Config2 = if IsSlave ->
+                     set_opt(peer_nick, ?config(master_nick, Config1), Config1);
+                IsMaster ->
+                     set_opt(peer_nick, ?config(slave_nick, Config1), Config1);
+                true ->
+                     Config1
+             end,
+    Config = if IsSlave -> set_opt(peer, Master, Config2);
+               IsMaster -> set_opt(peer, Slave, Config2);
+               true -> Config2
+            end,
     case Test of
         "test_connect" ++ _ ->
             Config;
@@ -320,6 +341,8 @@ no_db_tests() ->
        [sm,
        sm_resume,
        sm_resume_failed]},
+     muc_tests:single_cases(),
+     muc_tests:master_slave_cases(),
      {test_proxy65, [parallel],
       [proxy65_master, proxy65_slave]},
      {replaced, [parallel],
@@ -369,9 +392,9 @@ db_tests(riak) ->
        privacy,
        blocking,
        vcard,
+       muc_tests:single_cases(),
        test_unregister]},
-     {test_muc_register, [parallel],
-      [muc_register_master, muc_register_slave]},
+     muc_tests:master_slave_cases(),
      {test_roster_subscribe, [parallel],
       [roster_subscribe_master,
        roster_subscribe_slave]},
@@ -379,8 +402,6 @@ db_tests(riak) ->
       [flex_offline_master, flex_offline_slave]},
      {test_offline, [sequence],
       [offline_master, offline_slave]},
-     {test_muc, [parallel],
-      [muc_master, muc_slave]},
      {test_announce, [sequence],
       [announce_master, announce_slave]},
      {test_vcard_xupdate, [parallel],
@@ -403,10 +424,10 @@ db_tests(DB) when DB == mnesia; DB == redis ->
        blocking,
        vcard,
        pubsub_single_tests(),
+       muc_tests:single_cases(),
        test_unregister]},
+     muc_tests:master_slave_cases(),
      pubsub_multiple_tests(),
-     {test_muc_register, [parallel],
-      [muc_register_master, muc_register_slave]},
      {test_mix, [parallel],
       [mix_master, mix_slave]},
      {test_roster_subscribe, [parallel],
@@ -424,8 +445,6 @@ db_tests(DB) when DB == mnesia; DB == redis ->
       [carbons_master, carbons_slave]},
      {test_client_state, [parallel],
       [client_state_master, client_state_slave]},
-     {test_muc, [parallel],
-      [muc_master, muc_slave]},
      {test_muc_mam, [parallel],
       [muc_mam_master, muc_mam_slave]},
      {test_announce, [sequence],
@@ -440,21 +459,21 @@ db_tests(_) ->
     [{single_user, [sequence],
       [test_register,
        legacy_auth_tests(),
-       auth_plain,
-       auth_md5,
-       presence_broadcast,
-       last,
-       roster_get,
-       roster_ver,
-       private,
-       privacy,
-       blocking,
-       vcard,
-        pubsub_single_tests(),
-        test_unregister]},
+       auth_plain,
+       auth_md5,
+       presence_broadcast,
+       last,
+       roster_get,
+       roster_ver,
+       private,
+       privacy,
+       blocking,
+       vcard,
+       pubsub_single_tests(),
+       muc_tests:single_cases(),
+       test_unregister]},
+     muc_tests:master_slave_cases(),
      pubsub_multiple_tests(),
-     {test_muc_register, [parallel],
-      [muc_register_master, muc_register_slave]},
      {test_mix, [parallel],
       [mix_master, mix_slave]},
      {test_roster_subscribe, [parallel],
@@ -468,8 +487,6 @@ db_tests(_) ->
       [mam_old_master, mam_old_slave]},
      {test_new_mam, [parallel],
       [mam_new_master, mam_new_slave]},
-     {test_muc, [parallel],
-      [muc_master, muc_slave]},
      {test_muc_mam, [parallel],
       [muc_mam_master, muc_mam_slave]},
      {test_announce, [sequence],
@@ -604,7 +621,8 @@ test_connect_bad_ns_stream(Config) ->
     close_socket(Config0).
 
 test_connect_bad_lang(Config) ->
-    Config0 = init_stream(set_opt(lang, lists:duplicate(36, $x), Config)),
+    Lang = iolist_to_binary(lists:duplicate(36, $x)),
+    Config0 = init_stream(set_opt(lang, Lang, Config)),
     ?recv1(#stream_error{reason = 'policy-violation'}),
     ?recv1({xmlstreamend, <<"stream:stream">>}),
     close_socket(Config0).
@@ -2272,352 +2290,151 @@ muc_mam_master(Config) ->
 muc_mam_slave(Config) ->
     disconnect(Config).
 
-muc_master(Config) ->
-    MyJID = my_jid(Config),
-    PeerJID = ?config(slave, Config),
-    PeerBareJID = jid:remove_resource(PeerJID),
-    PeerJIDStr = jid:to_string(PeerJID),
-    MUC = muc_jid(Config),
-    Room = muc_room_jid(Config),
-    MyNick = ?config(master_nick, Config),
-    MyNickJID = jid:replace_resource(Room, MyNick),
-    PeerNick = ?config(slave_nick, Config),
-    PeerNickJID = jid:replace_resource(Room, PeerNick),
-    Subject = ?config(room_subject, Config),
-    Localhost = jid:make(<<"">>, <<"localhost">>, <<"">>),
-    true = is_feature_advertised(Config, ?NS_MUC, MUC),
-    %% Joining
-    send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}),
-    %% As per XEP-0045 we MUST receive stanzas in the following order:
-    %% 1. In-room presence from other occupants
-    %% 2. In-room presence from the joining entity itself (so-called "self-presence")
-    %% 3. Room history (if any)
-    %% 4. The room subject
-    %% 5. Live messages, presence updates, new user joins, etc.
-    %% As this is the newly created room, we receive only the 2nd stanza.
-    #muc_user{
-       status_codes = Codes,
-       items = [#muc_item{role = moderator,
-                         jid = MyJID,
-                         affiliation = owner}]} =
-       xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
-    %% 110 -> Inform user that presence refers to itself
-    %% 201 -> Inform user that a new room has been created
-    [110, 201] = lists:sort(Codes),
-    %% Request the configuration
-    #iq{type = result, sub_els = [#muc_owner{config = #xdata{} = RoomCfg}]} =
-        send_recv(Config, #iq{type = get, sub_els = [#muc_owner{}],
-                              to = Room}),
-    NewFields =
-        lists:flatmap(
-          fun(#xdata_field{var = Var, values = OrigVals}) ->
-                  Vals = case Var of
-                             <<"FORM_TYPE">> ->
-                                 OrigVals;
-                             <<"muc#roomconfig_roomname">> ->
-                                 [<<"Test room">>];
-                             <<"muc#roomconfig_roomdesc">> ->
-                                 [<<"Trying to break the server">>];
-                             <<"muc#roomconfig_persistentroom">> ->
-                                 [<<"1">>];
-                            <<"members_by_default">> ->
-                                [<<"0">>];
-                            <<"muc#roomconfig_allowvoicerequests">> ->
-                                [<<"1">>];
-                            <<"public_list">> ->
-                                [<<"1">>];
-                            <<"muc#roomconfig_publicroom">> ->
-                                [<<"1">>];
-                             _ ->
-                                 []
-                         end,
-                  if Vals /= [] ->
-                          [#xdata_field{values = Vals, var = Var}];
-                     true ->
-                          []
-                  end
-          end, RoomCfg#xdata.fields),
-    NewRoomCfg = #xdata{type = submit, fields = NewFields},
-    ID = send(Config, #iq{type = set, to = Room,
-                         sub_els = [#muc_owner{config = NewRoomCfg}]}),
-    ?recv2(#iq{type = result, id = ID},
-          #message{from = Room, type = groupchat,
-                   sub_els = [#muc_user{status_codes = [104]}]}),
-    %% Set subject
-    send(Config, #message{to = Room, type = groupchat,
-                          body = [#text{data = Subject}]}),
-    ?recv1(#message{from = MyNickJID, type = groupchat,
-             body = [#text{data = Subject}]}),
-    %% Sending messages (and thus, populating history for our peer)
-    lists:foreach(
-      fun(N) ->
-              Text = #text{data = integer_to_binary(N)},
-              I = send(Config, #message{to = Room, body = [Text],
-                                       type = groupchat}),
-             ?recv1(#message{from = MyNickJID, id = I,
-                      type = groupchat,
-                      body = [Text]})
-      end, lists:seq(1, 5)),
-    %% Inviting the peer
-    send(Config, #message{to = Room, type = normal,
-                         sub_els =
-                             [#muc_user{
-                                 invites =
-                                     [#muc_invite{to = PeerJID}]}]}),
-    #muc_user{
-       items = [#muc_item{role = visitor,
-                         jid = PeerJID,
-                         affiliation = none}]} =
-       xmpp:get_subtag(?recv1(#presence{from = PeerNickJID}), #muc_user{}),
-    %% Receiving a voice request
-    #message{from = Room,
-            sub_els = [#xdata{type = form,
-                              instructions = [_],
-                              fields = VoiceReqFs}]} = recv(Config),
-    %% Approving the voice request
-    ReplyVoiceReqFs =
-       lists:map(
-         fun(#xdata_field{var = Var, values = OrigVals}) ->
-                  Vals = case {Var, OrigVals} of
-                            {<<"FORM_TYPE">>,
-                             [<<"http://jabber.org/protocol/muc#request">>]} ->
-                                OrigVals;
-                            {<<"muc#role">>, [<<"participant">>]} ->
-                                [<<"participant">>];
-                            {<<"muc#jid">>, [PeerJIDStr]} ->
-                                [PeerJIDStr];
-                            {<<"muc#roomnick">>, [PeerNick]} ->
-                                [PeerNick];
-                            {<<"muc#request_allow">>, [<<"0">>]} ->
-                                [<<"1">>]
-                        end,
-                 #xdata_field{values = Vals, var = Var}
-         end, VoiceReqFs),
-    send(Config, #message{to = Room,
-                         sub_els = [#xdata{type = submit,
-                                           fields = ReplyVoiceReqFs}]}),
-    %% Peer is becoming a participant
-    #muc_user{items = [#muc_item{role = participant,
-                                jid = PeerJID,
-                                affiliation = none}]} =
-       xmpp:get_subtag(?recv1(#presence{from = PeerNickJID}), #muc_user{}),
-    %% Receive private message from the peer
-    ?recv1(#message{from = PeerNickJID, body = [#text{data = Subject}]}),
-    %% Granting membership to the peer and localhost server
-    I1 = send(Config,
-             #iq{type = set, to = Room,
-                 sub_els =
-                     [#muc_admin{
-                         items = [#muc_item{jid = Localhost,
-                                            affiliation = member},
-                                  #muc_item{nick = PeerNick,
-                                            jid = PeerBareJID,
-                                            affiliation = member}]}]}),
-    %% Peer became a member
-    #muc_user{items = [#muc_item{affiliation = member,
-                                jid = PeerJID,
-                                role = participant}]} =
-       xmpp:get_subtag(?recv1(#presence{from = PeerNickJID}), #muc_user{}),
-    ?recv1(#message{from = Room,
-             sub_els = [#muc_user{
-                           items = [#muc_item{affiliation = member,
-                                              jid = Localhost,
-                                              role = none}]}]}),
-    ?recv1(#iq{type = result, id = I1, sub_els = []}),
-    %% Receive groupchat message from the peer
-    ?recv1(#message{type = groupchat, from = PeerNickJID,
-            body = [#text{data = Subject}]}),
-    %% Retrieving a member list
-    #iq{type = result, sub_els = [#muc_admin{items = MemberList}]} =
-       send_recv(Config,
-                 #iq{type = get, to = Room,
-                     sub_els =
-                         [#muc_admin{items = [#muc_item{affiliation = member}]}]}),
-    [#muc_item{affiliation = member,
-              jid = Localhost},
-     #muc_item{affiliation = member,
-              jid = PeerBareJID}] = lists:keysort(#muc_item.jid, MemberList),
-    %% Kick the peer
-    I2 = send(Config,
-             #iq{type = set, to = Room,
-                 sub_els = [#muc_admin{
-                               items = [#muc_item{nick = PeerNick,
-                                                  role = none}]}]}),
-    %% Got notification the peer is kicked
-    %% 307 -> Inform user that he or she has been kicked from the room
-    ?recv1(#presence{from = PeerNickJID, type = unavailable,
-             sub_els = [#muc_user{
-                           status_codes = [307],
-                           items = [#muc_item{affiliation = member,
-                                              jid = PeerJID,
-                                              role = none}]}]}),
-    ?recv1(#iq{type = result, id = I2, sub_els = []}),
-    %% Destroying the room
-    I3 = send(Config,
-             #iq{type = set, to = Room,
-                 sub_els = [#muc_owner{
-                               destroy = #muc_destroy{
-                                            reason = Subject}}]}),
-    %% Kicked off
-    ?recv1(#presence{from = MyNickJID, type = unavailable,
-              sub_els = [#muc_user{items = [#muc_item{role = none,
-                                                     affiliation = none}],
-                                  destroy = #muc_destroy{
-                                               reason = Subject}}]}),
-    ?recv1(#iq{type = result, id = I3, sub_els = []}),
-    disconnect(Config).
-
-muc_slave(Config) ->
-    PeerJID = ?config(master, Config),
-    MUC = muc_jid(Config),
-    Room = muc_room_jid(Config),
-    MyNick = ?config(slave_nick, Config),
-    MyNickJID = jid:replace_resource(Room, MyNick),
-    PeerNick = ?config(master_nick, Config),
-    PeerNickJID = jid:replace_resource(Room, PeerNick),
-    Subject = ?config(room_subject, Config),
-    %% Receive an invite from the peer
-    #muc_user{invites = [#muc_invite{from = PeerJID}]} =
-       xmpp:get_subtag(?recv1(#message{from = Room, type = normal}),
-                       #muc_user{}),
-    %% But before joining we discover the MUC service first
-    %% to check if the room is in the disco list
-    #iq{type = result,
-       sub_els = [#disco_items{items = [#disco_item{jid = Room}]}]} =
-       send_recv(Config, #iq{type = get, to = MUC,
-                             sub_els = [#disco_items{}]}),
-    %% Now check if the peer is in the room. We check this via disco#items
-    #iq{type = result,
-       sub_els = [#disco_items{items = [#disco_item{jid = PeerNickJID,
-                                                    name = PeerNick}]}]} =
-       send_recv(Config, #iq{type = get, to = Room,
-                             sub_els = [#disco_items{}]}),
-    %% Now joining
-    send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}),
-    %% First presence is from the participant, i.e. from the peer
-    #muc_user{
-       status_codes = [],
-       items = [#muc_item{role = moderator,
-                         affiliation = owner}]} =
-       xmpp:get_subtag(?recv1(#presence{from = PeerNickJID}), #muc_user{}),
-    %% The next is the self-presence (code 110 means it)
-    #muc_user{status_codes = [110],
-             items = [#muc_item{role = visitor,
-                                affiliation = none}]} =
-       xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
-    %% Receive the room subject
-    ?recv1(#message{from = PeerNickJID, type = groupchat,
-             body = [#text{data = Subject}],
-            sub_els = [#delay{}]}),
-    %% Receive MUC history
-    lists:foreach(
-      fun(N) ->
-              Text = #text{data = integer_to_binary(N)},
-             ?recv1(#message{from = PeerNickJID,
-                      type = groupchat,
-                      body = [Text],
-                      sub_els = [#delay{}]})
-      end, lists:seq(1, 5)),
-    %% Sending a voice request
-    VoiceReq = #xdata{
-                 type = submit,
-                 fields =
-                     [#xdata_field{
-                         var = <<"FORM_TYPE">>,
-                         values = [<<"http://jabber.org/protocol/muc#request">>]},
-                      #xdata_field{
-                         var = <<"muc#role">>,
-                         type = 'text-single',
-                         values = [<<"participant">>]}]},
-    send(Config, #message{to = Room, sub_els = [VoiceReq]}),
-    %% Becoming a participant
-    #muc_user{items = [#muc_item{role = participant,
-                                affiliation = none}]} =
-       xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
-    %% Sending private message to the peer
-    send(Config, #message{to = PeerNickJID,
-                         body = [#text{data = Subject}]}),
-    %% Becoming a member
-    #muc_user{items = [#muc_item{role = participant,
-                                affiliation = member}]} =
-       xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
-    %% Sending groupchat message
-    send(Config, #message{to = Room, type = groupchat,
-                         body = [#text{data = Subject}]}),
-    %% Receive this message back
-    ?recv1(#message{type = groupchat, from = MyNickJID,
-            body = [#text{data = Subject}]}),
-    %% We're kicked off
-    %% 307 -> Inform user that he or she has been kicked from the room
-    ?recv1(#presence{from = MyNickJID, type = unavailable,
-             sub_els = [#muc_user{
-                           status_codes = [307],
-                           items = [#muc_item{affiliation = member,
-                                              role = none}]}]}),
-    disconnect(Config).
-
-muc_register_nick(Config, MUC, PrevNick, Nick) ->
-    PrevRegistered = if PrevNick /= <<"">> -> true;
-                       true -> false
-                    end,
-    NewRegistered = if Nick /= <<"">> -> true;
-                      true -> false
-                   end,
-    %% Request register form
-    #iq{type = result,
-       sub_els = [#register{registered = PrevRegistered,
-                            xdata = #xdata{type = form,
-                                           fields = FsWithoutNick}}]} =
-       send_recv(Config, #iq{type = get, to = MUC,
-                             sub_els = [#register{}]}),
-    %% Check if previous nick is registered
-    PrevNick = proplists:get_value(
-                roomnick, muc_register:decode(FsWithoutNick)),
-    X = #xdata{type = submit, fields = muc_register:encode([{roomnick, Nick}])},
-    %% Submitting form
-    #iq{type = result, sub_els = []} =
-       send_recv(Config, #iq{type = set, to = MUC,
-                             sub_els = [#register{xdata = X}]}),
-    %% Check if new nick was registered
-    #iq{type = result,
-       sub_els = [#register{registered = NewRegistered,
-                            xdata = #xdata{type = form,
-                                           fields = FsWithNick}}]} =
-       send_recv(Config, #iq{type = get, to = MUC,
-                             sub_els = [#register{}]}),
-    Nick = proplists:get_value(
-            roomnick, muc_register:decode(FsWithNick)).
+%% OK, I know this is retarded, but I didn't find a better way to
+%% split the test cases into different modules
+muc_service_presence_error(Config) ->
+    muc_tests:muc_service_presence_error(Config).
+muc_service_message_error(Config) ->
+    muc_tests:muc_service_message_error(Config).
+muc_service_unknown_ns_iq_error(Config) ->
+    muc_tests:muc_service_unknown_ns_iq_error(Config).
+muc_service_iq_set_error(Config) ->
+    muc_tests:muc_service_iq_set_error(Config).
+muc_service_improper_iq_error(Config) ->
+    muc_tests:muc_service_improper_iq_error(Config).
+muc_service_features(Config) ->
+    muc_tests:muc_service_features(Config).
+muc_service_disco_info_node_error(Config) ->
+    muc_tests:muc_service_disco_info_node_error(Config).
+muc_service_disco_items(Config) ->
+    muc_tests:muc_service_disco_items(Config).
+muc_service_vcard(Config) ->
+    muc_tests:muc_service_vcard(Config).
+muc_service_unique(Config) ->
+    muc_tests:muc_service_unique(Config).
+muc_service_subscriptions(Config) ->
+    muc_tests:muc_service_subscriptions(Config).
+muc_configure_non_existent(Config) ->
+    muc_tests:muc_configure_non_existent(Config).
+muc_cancel_configure_non_existent(Config) ->
+    muc_tests:muc_cancel_configure_non_existent(Config).
 
 muc_register_master(Config) ->
-    MUC = muc_jid(Config),
-    %% Register nick "master1"
-    muc_register_nick(Config, MUC, <<"">>, <<"master1">>),
-    %% Unregister nick "master1" via jabber:register
-    #iq{type = result, sub_els = []} =
-       send_recv(Config, #iq{type = set, to = MUC,
-                             sub_els = [#register{remove = true}]}),
-    %% Register nick "master2"
-    muc_register_nick(Config, MUC, <<"">>, <<"master2">>),
-    %% Now register nick "master"
-    muc_register_nick(Config, MUC, <<"master2">>, <<"master">>),
-    %% Wait for slave to fail trying to register nick "master"
-    wait_for_slave(Config),
-    wait_for_slave(Config),
-    %% Now register empty ("") nick, which means we're unregistering
-    muc_register_nick(Config, MUC, <<"master">>, <<"">>),
-    disconnect(Config).
-
+    muc_tests:muc_register_master(Config).
 muc_register_slave(Config) ->
-    MUC = muc_jid(Config),
-    wait_for_master(Config),
-    %% Trying to register occupied nick "master"
-    Fs = muc_register:encode([{roomnick, <<"master">>}]),
-    X = #xdata{type = submit, fields = Fs},
-    #iq{type = error} =
-       send_recv(Config, #iq{type = set, to = MUC,
-                             sub_els = [#register{xdata = X}]}),
-    wait_for_master(Config),
-    disconnect(Config).
+    muc_tests:muc_register_slave(Config).
+muc_join_conflict_master(Config) ->
+    muc_tests:muc_join_conflict_master(Config).
+muc_join_conflict_slave(Config) ->
+    muc_tests:muc_join_conflict_slave(Config).
+muc_groupchat_msg_master(Config) ->
+    muc_tests:muc_groupchat_msg_master(Config).
+muc_groupchat_msg_slave(Config) ->
+    muc_tests:muc_groupchat_msg_slave(Config).
+muc_private_msg_master(Config) ->
+    muc_tests:muc_private_msg_master(Config).
+muc_private_msg_slave(Config) ->
+    muc_tests:muc_private_msg_slave(Config).
+muc_set_subject_master(Config) ->
+    muc_tests:muc_set_subject_master(Config).
+muc_set_subject_slave(Config) ->
+    muc_tests:muc_set_subject_slave(Config).
+muc_history_master(Config) ->
+    muc_tests:muc_history_master(Config).
+muc_history_slave(Config) ->
+    muc_tests:muc_history_slave(Config).
+muc_invite_master(Config) ->
+    muc_tests:muc_invite_master(Config).
+muc_invite_slave(Config) ->
+    muc_tests:muc_invite_slave(Config).
+muc_invite_members_only_master(Config) ->
+    muc_tests:muc_invite_members_only_master(Config).
+muc_invite_members_only_slave(Config) ->
+    muc_tests:muc_invite_members_only_slave(Config).
+muc_invite_password_protected_master(Config) ->
+    muc_tests:muc_invite_password_protected_master(Config).
+muc_invite_password_protected_slave(Config) ->
+    muc_tests:muc_invite_password_protected_slave(Config).
+muc_voice_request_master(Config) ->
+    muc_tests:muc_voice_request_master(Config).
+muc_voice_request_slave(Config) ->
+    muc_tests:muc_voice_request_slave(Config).
+muc_change_role_master(Config) ->
+    muc_tests:muc_change_role_master(Config).
+muc_change_role_slave(Config) ->
+    muc_tests:muc_change_role_slave(Config).
+muc_kick_master(Config) ->
+    muc_tests:muc_kick_master(Config).
+muc_kick_slave(Config) ->
+    muc_tests:muc_kick_slave(Config).
+muc_change_affiliation_master(Config) ->
+    muc_tests:muc_change_affiliation_master(Config).
+muc_change_affiliation_slave(Config) ->
+    muc_tests:muc_change_affiliation_slave(Config).
+muc_destroy_master(Config) ->
+    muc_tests:muc_destroy_master(Config).
+muc_destroy_slave(Config) ->
+    muc_tests:muc_destroy_slave(Config).
+muc_vcard_master(Config) ->
+    muc_tests:muc_vcard_master(Config).
+muc_vcard_slave(Config) ->
+    muc_tests:muc_vcard_slave(Config).
+muc_nick_change_master(Config) ->
+    muc_tests:muc_nick_change_master(Config).
+muc_nick_change_slave(Config) ->
+    muc_tests:muc_nick_change_slave(Config).
+muc_config_title_desc_master(Config) ->
+    muc_tests:muc_config_title_desc_master(Config).
+muc_config_title_desc_slave(Config) ->
+    muc_tests:muc_config_title_desc_slave(Config).
+muc_config_public_list_master(Config) ->
+    muc_tests:muc_config_public_list_master(Config).
+muc_config_public_list_slave(Config) ->
+    muc_tests:muc_config_public_list_slave(Config).
+muc_config_password_master(Config) ->
+    muc_tests:muc_config_password_master(Config).
+muc_config_password_slave(Config) ->
+    muc_tests:muc_config_password_slave(Config).
+muc_config_whois_master(Config) ->
+    muc_tests:muc_config_whois_master(Config).
+muc_config_whois_slave(Config) ->
+    muc_tests:muc_config_whois_slave(Config).
+muc_config_members_only_master(Config) ->
+    muc_tests:muc_config_members_only_master(Config).
+muc_config_members_only_slave(Config) ->
+    muc_tests:muc_config_members_only_slave(Config).
+muc_config_moderated_master(Config) ->
+    muc_tests:muc_config_moderated_master(Config).
+muc_config_moderated_slave(Config) ->
+    muc_tests:muc_config_moderated_slave(Config).
+muc_config_private_messages_master(Config) ->
+    muc_tests:muc_config_private_messages_master(Config).
+muc_config_private_messages_slave(Config) ->
+    muc_tests:muc_config_private_messages_slave(Config).
+muc_config_query_master(Config) ->
+    muc_tests:muc_config_query_master(Config).
+muc_config_query_slave(Config) ->
+    muc_tests:muc_config_query_slave(Config).
+muc_config_allow_invites_master(Config) ->
+    muc_tests:muc_config_allow_invites_master(Config).
+muc_config_allow_invites_slave(Config) ->
+    muc_tests:muc_config_allow_invites_slave(Config).
+muc_config_visitor_status_master(Config) ->
+    muc_tests:muc_config_visitor_status_master(Config).
+muc_config_visitor_status_slave(Config) ->
+    muc_tests:muc_config_visitor_status_slave(Config).
+muc_config_allow_voice_requests_master(Config) ->
+    muc_tests:muc_config_allow_voice_requests_master(Config).
+muc_config_allow_voice_requests_slave(Config) ->
+    muc_tests:muc_config_allow_voice_requests_slave(Config).
+muc_config_voice_request_interval_master(Config) ->
+    muc_tests:muc_config_voice_request_interval_master(Config).
+muc_config_voice_request_interval_slave(Config) ->
+    muc_tests:muc_config_voice_request_interval_slave(Config).
+muc_config_visitor_nickchange_master(Config) ->
+    muc_tests:muc_config_visitor_nickchange_master(Config).
+muc_config_visitor_nickchange_slave(Config) ->
+    muc_tests:muc_config_visitor_nickchange_slave(Config).
 
 announce_master(Config) ->
     MyJID = my_jid(Config),
index 2d2e098dea22111f8082a6894059e130649544c4..3a6d4947f972a9dd29baa43adc35cfd1fbc3b581 100644 (file)
@@ -443,6 +443,7 @@ modules:
   mod_ping: []
   mod_proxy65: []
   mod_legacy: []
+  mod_muc: []
   mod_register: 
     welcome_message: 
       subject: "Welcome!"
diff --git a/test/muc_tests.erl b/test/muc_tests.erl
new file mode 100644 (file)
index 0000000..c89b977
--- /dev/null
@@ -0,0 +1,1877 @@
+%%%-------------------------------------------------------------------
+%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%% @copyright (C) 2016, Evgeny Khramtsov
+%%% @doc
+%%%
+%%% @end
+%%% Created : 15 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
+%%%-------------------------------------------------------------------
+-module(muc_tests).
+
+%% API
+-compile(export_all).
+-import(suite, [recv_presence/1, send_recv/2, my_jid/1, muc_room_jid/1,
+               send/2, recv_message/1, recv_iq/1, recv/1, muc_jid/1,
+               alt_room_jid/1, wait_for_slave/1, wait_for_master/1,
+               disconnect/1, put_event/2, get_event/1, peer_muc_jid/1,
+               my_muc_jid/1, get_features/2, flush/1, set_opt/3]).
+-include("suite.hrl").
+-include("jid.hrl").
+
+%%%===================================================================
+%%% API
+%%%===================================================================
+%%%===================================================================
+%%% Single tests
+%%%===================================================================
+single_cases() ->
+    {muc_single, [sequence],
+     [muc_service_presence_error,
+      muc_service_message_error,
+      muc_service_unknown_ns_iq_error,
+      muc_service_iq_set_error,
+      muc_service_improper_iq_error,
+      muc_service_features,
+      muc_service_disco_info_node_error,
+      muc_service_disco_items,
+      muc_service_unique,
+      muc_service_vcard,
+      muc_configure_non_existent,
+      muc_cancel_configure_non_existent,
+      muc_service_subscriptions]}.
+
+muc_service_presence_error(Config) ->
+    Service = muc_jid(Config),
+    ServiceResource = jid:replace_resource(Service, randoms:get_string()),
+    lists:foreach(
+      fun(To) ->
+             send(Config, #presence{type = error, to = To}),
+             lists:foreach(
+               fun(Type) ->
+                       #presence{type = error} = Err =
+                           send_recv(Config, #presence{type = Type, to = To}),
+                       #stanza_error{reason = 'service-unavailable'} =
+                           xmpp:get_error(Err)
+               end, [available, unavailable])
+      end, [Service, ServiceResource]),
+    disconnect(Config).
+
+muc_service_message_error(Config) ->
+    Service = muc_jid(Config),
+    send(Config, #message{type = error, to = Service}),
+    lists:foreach(
+      fun(Type) ->
+             #message{type = error} = Err1 =
+                 send_recv(Config, #message{type = Type, to = Service}),
+             #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err1)
+      end, [chat, normal, headline, groupchat]),
+    ServiceResource = jid:replace_resource(Service, randoms:get_string()),
+    send(Config, #message{type = error, to = ServiceResource}),
+    lists:foreach(
+      fun(Type) ->
+             #message{type = error} = Err2 =
+                 send_recv(Config, #message{type = Type, to = ServiceResource}),
+             #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err2)
+      end, [chat, normal, headline, groupchat]),
+    disconnect(Config).
+
+muc_service_unknown_ns_iq_error(Config) ->
+    Service = muc_jid(Config),
+    ServiceResource = jid:replace_resource(Service, randoms:get_string()),
+    lists:foreach(
+      fun(To) ->
+             send(Config, #iq{type = result, to = To}),
+             send(Config, #iq{type = error, to = To}),
+             lists:foreach(
+               fun(Type) ->
+                       #iq{type = error} = Err1 =
+                           send_recv(Config, #iq{type = Type, to = To,
+                                                 sub_els = [#presence{}]}),
+                       #stanza_error{reason = 'service-unavailable'} =
+                           xmpp:get_error(Err1)
+               end, [set, get])
+      end, [Service, ServiceResource]),
+    disconnect(Config).
+
+muc_service_iq_set_error(Config) ->
+    Service = muc_jid(Config),
+    lists:foreach(
+      fun(SubEl) ->
+             send(Config, #iq{type = result, to = Service,
+                              sub_els = [SubEl]}),
+             #iq{type = error} = Err2 =
+                 send_recv(Config, #iq{type = set, to = Service,
+                                       sub_els = [SubEl]}),
+             #stanza_error{reason = 'not-allowed'} =
+                 xmpp:get_error(Err2)
+      end, [#disco_items{}, #disco_info{}, #vcard_temp{},
+           #muc_unique{}, #muc_subscriptions{}]),
+    disconnect(Config).
+
+muc_service_improper_iq_error(Config) ->
+    Service = muc_jid(Config),
+    lists:foreach(
+      fun(SubEl) ->
+             send(Config, #iq{type = result, to = Service,
+                              sub_els = [SubEl]}),
+             lists:foreach(
+               fun(Type) ->
+                       #iq{type = error} = Err3 =
+                           send_recv(Config, #iq{type = Type, to = Service,
+                                                 sub_els = [SubEl]}),
+                       #stanza_error{reason = Reason} = xmpp:get_error(Err3),
+                       true = Reason /= 'internal-server-error'
+               end, [set, get])
+      end, [#disco_item{jid = Service},
+           #identity{category = <<"category">>, type = <<"type">>},
+           #vcard_email{}, #muc_subscribe{nick = ?config(nick, Config)}]),
+    disconnect(Config).
+
+muc_service_features(Config) ->
+    ServerHost = ?config(server_host, Config),
+    MUC = muc_jid(Config),
+    Features = sets:from_list(get_features(Config, MUC)),
+    MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of
+                     true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1];
+                     false -> []
+                 end,
+    RequiredFeatures = sets:from_list(
+                        [?NS_DISCO_INFO, ?NS_DISCO_ITEMS,
+                         ?NS_REGISTER, ?NS_MUC, ?NS_RSM,
+                         ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE
+                         | MAMFeatures]),
+    ct:comment("Checking if all needed disco features are set"),
+    true = sets:is_subset(RequiredFeatures, Features),
+    disconnect(Config).
+
+muc_service_disco_info_node_error(Config) ->
+    MUC = muc_jid(Config),
+    Node = randoms:get_string(),
+    #iq{type = error} = Err =
+       send_recv(Config, #iq{type = get, to = MUC,
+                             sub_els = [#disco_info{node = Node}]}),
+    #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err),
+    disconnect(Config).
+
+muc_service_disco_items(Config) ->
+    #jid{server = Service} = muc_jid(Config),
+    Rooms = lists:sort(
+             lists:map(
+               fun(I) ->
+                       RoomName = integer_to_binary(I),
+                       jid:make(RoomName, Service)
+               end, lists:seq(1, 5))),
+    lists:foreach(
+      fun(Room) ->
+             ok = muc_join_new(Config, Room)
+      end, Rooms),
+    Items = muc_disco_items(Config),
+    Rooms = [J || #disco_item{jid = J} <- Items],
+    lists:foreach(
+      fun(Room) ->
+             ok = muc_leave(Config, Room)
+      end, Rooms),
+    [] = muc_disco_items(Config),
+    disconnect(Config).
+
+muc_service_vcard(Config) ->
+    MUC = muc_jid(Config),
+    ct:comment("Retreiving vCard from ~s", [jid:to_string(MUC)]),
+    #iq{type = result, sub_els = [#vcard_temp{}]} =
+       send_recv(Config, #iq{type = get, to = MUC, sub_els = [#vcard_temp{}]}),
+    disconnect(Config).
+
+muc_service_unique(Config) ->
+    MUC = muc_jid(Config),
+    ct:comment("Requesting muc unique from ~s", [jid:to_string(MUC)]),
+    #iq{type = result, sub_els = [#muc_unique{name = Name}]} =
+       send_recv(Config, #iq{type = get, to = MUC, sub_els = [#muc_unique{}]}),
+    ct:comment("Checking if unique name is set in the response"),
+    <<_, _/binary>> = Name,
+    disconnect(Config).
+
+muc_configure_non_existent(Config) ->
+    [_|_] = muc_get_config(Config),
+    disconnect(Config).
+
+muc_cancel_configure_non_existent(Config) ->
+    Room = muc_room_jid(Config),
+    #iq{type = result, sub_els = []} =
+       send_recv(Config,
+                 #iq{to = Room, type = set,
+                     sub_els = [#muc_owner{config = #xdata{type = cancel}}]}),
+    disconnect(Config).
+
+muc_service_subscriptions(Config) ->
+    MUC = #jid{server = Service} = muc_jid(Config),
+    Rooms = lists:sort(
+             lists:map(
+               fun(I) ->
+                       RoomName = integer_to_binary(I),
+                       jid:make(RoomName, Service)
+               end, lists:seq(1, 5))),
+    lists:foreach(
+      fun(Room) ->
+             ok = muc_join_new(Config, Room),
+             [104] = muc_set_config(Config, [{allow_subscription, true}], Room),
+             [] = muc_subscribe(Config, [], Room)
+      end, Rooms),
+    #iq{type = result, sub_els = [#muc_subscriptions{list = JIDs}]} =
+       send_recv(Config, #iq{type = get, to = MUC,
+                             sub_els = [#muc_subscriptions{}]}),
+    Rooms = lists:sort(JIDs),
+    lists:foreach(
+      fun(Room) ->
+             ok = muc_unsubscribe(Config, Room),
+             ok = muc_leave(Config, Room)
+      end, Rooms),
+    disconnect(Config).
+
+%%%===================================================================
+%%% Master-slave tests
+%%%===================================================================
+master_slave_cases() ->
+    {muc_master_slave, [sequence],
+     [master_slave_test(muc_register),
+      master_slave_test(muc_groupchat_msg),
+      master_slave_test(muc_private_msg),
+      master_slave_test(muc_set_subject),
+      master_slave_test(muc_history),
+      master_slave_test(muc_invite),
+      master_slave_test(muc_invite_members_only),
+      master_slave_test(muc_invite_password_protected),
+      master_slave_test(muc_voice_request),
+      master_slave_test(muc_change_role),
+      master_slave_test(muc_kick),
+      master_slave_test(muc_change_affiliation),
+      master_slave_test(muc_destroy),
+      master_slave_test(muc_vcard),
+      master_slave_test(muc_nick_change),
+      master_slave_test(muc_config_title_desc),
+      master_slave_test(muc_config_public_list),
+      master_slave_test(muc_config_password),
+      master_slave_test(muc_config_whois),
+      master_slave_test(muc_config_members_only),
+      master_slave_test(muc_config_moderated),
+      master_slave_test(muc_config_private_messages),
+      master_slave_test(muc_config_query),
+      master_slave_test(muc_config_allow_invites),
+      master_slave_test(muc_config_visitor_status),
+      master_slave_test(muc_config_allow_voice_requests),
+      master_slave_test(muc_config_voice_request_interval),
+      master_slave_test(muc_config_visitor_nickchange),
+      master_slave_test(muc_join_conflict)]}.
+
+muc_join_conflict_master(Config) ->
+    ok = muc_join_new(Config),
+    put_event(Config, join),
+    ct:comment("Waiting for 'leave' command from the slave"),
+    leave = get_event(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_join_conflict_slave(Config) ->
+    NewConfig = set_opt(nick, ?config(peer_nick, Config), Config),
+    ct:comment("Waiting for 'join' command from the master"),
+    join = get_event(Config),
+    ct:comment("Fail trying to join the room with conflicting nick"),
+    #stanza_error{reason = 'conflict'} = muc_join(NewConfig),
+    put_event(Config, leave),
+    disconnect(NewConfig).
+
+muc_groupchat_msg_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(slave, Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    ok = muc_master_join(Config),
+    lists:foreach(
+      fun(I) ->
+             Body = xmpp:mk_text(integer_to_binary(I)),
+             send(Config, #message{type = groupchat, to = Room,
+                                   body = Body}),
+             #message{type = groupchat, from = MyNickJID,
+                      body = Body} = recv_message(Config)
+      end, lists:seq(1, 5)),
+    #muc_user{items = [#muc_item{jid = PeerJID,
+                                role = none,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_groupchat_msg_slave(Config) ->
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(master_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    {[], _, _} = muc_slave_join(Config),
+    lists:foreach(
+      fun(I) ->
+             Body = xmpp:mk_text(integer_to_binary(I)),
+             #message{type = groupchat, from = PeerNickJID,
+                      body = Body} = recv_message(Config)
+      end, lists:seq(1, 5)),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_private_msg_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(slave, Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ok = muc_master_join(Config),
+    lists:foreach(
+      fun(I) ->
+             Body = xmpp:mk_text(integer_to_binary(I)),
+             send(Config, #message{type = chat, to = PeerNickJID,
+                                   body = Body})
+      end, lists:seq(1, 5)),
+    #muc_user{items = [#muc_item{jid = PeerJID,
+                                role = none,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, unavailable),
+    ct:comment("Fail trying to send a private message to non-existing occupant"),
+    send(Config, #message{type = chat, to = PeerNickJID}),
+    #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config),
+    #stanza_error{reason = 'item-not-found'} = xmpp:get_error(ErrMsg),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_private_msg_slave(Config) ->
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(master_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    {[], _, _} = muc_slave_join(Config),
+    lists:foreach(
+      fun(I) ->
+             Body = xmpp:mk_text(integer_to_binary(I)),
+             #message{type = chat, from = PeerNickJID,
+                      body = Body} = recv_message(Config)
+      end, lists:seq(1, 5)),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_set_subject_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(slave, Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    Subject1 = xmpp:mk_text(?config(room_subject, Config)),
+    Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>),
+    ok = muc_master_join(Config),
+    ct:comment("Setting 1st subject"),
+    send(Config, #message{type = groupchat, to = Room,
+                         subject = Subject1}),
+    #message{type = groupchat, from = MyNickJID,
+            subject = Subject1} = recv_message(Config),
+    ct:comment("Waiting for the slave to leave"),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ct:comment("Setting 2nd subject"),
+    send(Config, #message{type = groupchat, to = Room,
+                         subject = Subject2}),
+    #message{type = groupchat, from = MyNickJID,
+            subject = Subject2} = recv_message(Config),
+    ct:comment("Asking the slave to join"),
+    put_event(Config, join),
+    recv_muc_presence(Config, PeerNickJID, available),
+    ct:comment("Receiving 1st subject set by the slave"),
+    #message{type = groupchat, from = PeerNickJID,
+            subject = Subject1} = recv_message(Config),
+    ct:comment("Disallow subject change"),
+    [104] = muc_set_config(Config, [{changesubject, false}]),
+    ct:comment("Waiting for the slave to leave"),
+    #muc_user{items = [#muc_item{jid = PeerJID,
+                                role = none,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_set_subject_slave(Config) ->
+    Room = muc_room_jid(Config),
+    MyNickJID = my_muc_jid(Config),
+    PeerNick = ?config(master_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    Subject1 = xmpp:mk_text(?config(room_subject, Config)),
+    Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>),
+    {[], _, _} = muc_slave_join(Config),
+    ct:comment("Receiving 1st subject set by the master"),
+    #message{type = groupchat, from = PeerNickJID,
+            subject = Subject1} = recv_message(Config),
+    ok = muc_leave(Config),
+    ct:comment("Waiting for 'join' command from the master"),
+    join = get_event(Config),
+    {[], SubjMsg2, _} = muc_join(Config),
+    ct:comment("Checking if the master has set 2nd subject during our absence"),
+    #message{type = groupchat, from = PeerNickJID,
+            subject = Subject2} = SubjMsg2,
+    ct:comment("Setting 1st subject"),
+    send(Config, #message{to = Room, type = groupchat, subject = Subject1}),
+    #message{type = groupchat, from = MyNickJID,
+            subject = Subject1} = recv_message(Config),
+    ct:comment("Waiting for the master to disallow subject change"),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Fail trying to change the subject"),
+    send(Config, #message{to = Room, type = groupchat, subject = Subject2}),
+    #message{from = Room, type = error} = ErrMsg = recv_message(Config),
+    #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_history_master(Config) ->
+    Room = muc_room_jid(Config),
+    ServerHost = ?config(server_host, Config),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    Size = gen_mod:get_module_opt(ServerHost, mod_muc, history_size,
+                                 fun(I) when is_integer(I), I>=0 -> I end,
+                                 20),
+    ok = muc_join_new(Config),
+    ct:comment("Putting ~p+1 messages in the history", [Size]),
+    %% Only Size messages will be stored
+    lists:foreach(
+      fun(I) ->
+             Body = xmpp:mk_text(integer_to_binary(I)),
+             send(Config, #message{to = Room, type = groupchat,
+                                   body = Body}),
+             #message{type = groupchat, from = MyNickJID,
+                      body = Body} = recv_message(Config)
+      end, lists:seq(0, Size)),
+    wait_for_slave(Config),
+    wait_for_slave(Config),
+    flush(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_history_slave(Config) ->
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(peer_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ServerHost = ?config(server_host, Config),
+    Size = gen_mod:get_module_opt(ServerHost, mod_muc, history_size,
+                                 fun(I) when is_integer(I), I>=0 -> I end,
+                                 20),
+    {History, _, _} = muc_slave_join(Config),
+    ct:comment("Checking ordering of history events"),
+    BodyList = [binary_to_integer(xmpp:get_text(Body))
+               || #message{type = groupchat, from = From,
+                           body = Body} <- History,
+                  From == PeerNickJID],
+    BodyList = lists:seq(1, Size),
+    ok = muc_leave(Config),
+    %% If the client wishes to receive no history, it MUST set the 'maxchars'
+    %% attribute to a value of "0" (zero)
+    %% (http://xmpp.org/extensions/xep-0045.html#enter-managehistory)
+    ct:comment("Checking if maxchars=0 yields to no history"),
+    {[], _, _} = muc_join(Config, #muc{history = #muc_history{maxchars = 0}}),
+    ok = muc_leave(Config),
+    ct:comment("Receiving only 10 last stanzas"),
+    {History10, _, _} = muc_join(Config,
+                                #muc{history = #muc_history{maxstanzas = 10}}),
+    BodyList10 = [binary_to_integer(xmpp:get_text(Body))
+                 || #message{type = groupchat, from = From,
+                             body = Body} <- History10,
+                    From == PeerNickJID],
+    BodyList10 = lists:nthtail(Size-10, lists:seq(1, Size)),
+    ok = muc_leave(Config),
+    #delay{stamp = TS} = xmpp:get_subtag(hd(History), #delay{}),
+    ct:comment("Receiving all history without the very first element"),
+    {HistoryWithoutFirst, _, _} = muc_join(Config,
+                                          #muc{history = #muc_history{since = TS}}),
+    BodyListWithoutFirst = [binary_to_integer(xmpp:get_text(Body))
+                           || #message{type = groupchat, from = From,
+                                       body = Body} <- HistoryWithoutFirst,
+                              From == PeerNickJID],
+    BodyListWithoutFirst = lists:nthtail(1, lists:seq(1, Size)),
+    ok = muc_leave(Config),
+    wait_for_master(Config),
+    disconnect(Config).
+
+muc_invite_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(peer, Config),
+    ok = muc_join_new(Config),
+    wait_for_slave(Config),
+    %% Inviting the peer
+    send(Config, #message{to = Room, type = normal,
+                         sub_els =
+                             [#muc_user{
+                                 invites =
+                                     [#muc_invite{to = PeerJID}]}]}),
+    #message{from = Room} = DeclineMsg = recv_message(Config),
+    #muc_user{decline = #muc_decline{from = PeerJID}} =
+       xmpp:get_subtag(DeclineMsg, #muc_user{}),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_invite_slave(Config) ->
+    Room = muc_room_jid(Config),
+    wait_for_master(Config),
+    PeerJID = ?config(master, Config),
+    #message{from = Room, type = normal} = Msg = recv_message(Config),
+    #muc_user{invites = [#muc_invite{from = PeerJID}]} =
+       xmpp:get_subtag(Msg, #muc_user{}),
+    %% Decline invitation
+    send(Config,
+        #message{to = Room,
+                 sub_els = [#muc_user{
+                               decline = #muc_decline{to = PeerJID}}]}),
+    disconnect(Config).
+
+muc_invite_members_only_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(slave, Config),
+    ok = muc_join_new(Config),
+    %% Setting the room to members-only
+    [_|_] = muc_set_config(Config, [{membersonly, true}]),
+    wait_for_slave(Config),
+    %% Inviting the peer
+    send(Config, #message{to = Room, type = normal,
+                         sub_els =
+                             [#muc_user{
+                                 invites =
+                                     [#muc_invite{to = PeerJID}]}]}),
+    #message{from = Room, type = normal} = AffMsg = recv_message(Config),
+    #muc_user{items = [#muc_item{jid = PeerJID, affiliation = member}]} =
+       xmpp:get_subtag(AffMsg, #muc_user{}),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_invite_members_only_slave(Config) ->
+    Room = muc_room_jid(Config),
+    wait_for_master(Config),
+    %% Receiving invitation
+    #message{from = Room, type = normal} = recv_message(Config),
+    disconnect(Config).
+
+muc_invite_password_protected_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(slave, Config),
+    Password = randoms:get_string(),
+    ok = muc_join_new(Config),
+    [104] = muc_set_config(Config, [{passwordprotectedroom, true},
+                                    {roomsecret, Password}]),
+    put_event(Config, Password),
+    %% Inviting the peer
+    send(Config, #message{to = Room, type = normal,
+                         sub_els =
+                             [#muc_user{
+                                 invites =
+                                     [#muc_invite{to = PeerJID}]}]}),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_invite_password_protected_slave(Config) ->
+    Room = muc_room_jid(Config),
+    Password = get_event(Config),
+    %% Receiving invitation
+    #message{from = Room, type = normal} = Msg = recv_message(Config),
+    #muc_user{password = Password} = xmpp:get_subtag(Msg, #muc_user{}),
+    disconnect(Config).
+
+muc_voice_request_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(slave, Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ok = muc_join_new(Config),
+    [104] = muc_set_config(Config, [{members_by_default, false}]),
+    wait_for_slave(Config),
+    #muc_user{
+       items = [#muc_item{role = visitor,
+                         jid = PeerJID,
+                         affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    ct:comment("Receiving voice request"),
+    #message{from = Room, type = normal} = VoiceReq = recv_message(Config),
+    #xdata{type = form, fields = Fs} = xmpp:get_subtag(VoiceReq, #xdata{}),
+    [{jid, PeerJID},
+     {request_allow, false},
+     {role, participant},
+     {roomnick, PeerNick}] = lists:sort(muc_request:decode(Fs)),
+    ct:comment("Approving voice request"),
+    ApprovalFs = muc_request:encode([{jid, PeerJID}, {role, participant},
+                                    {nick, PeerNick}, {request_allow, true}]),
+    send(Config, #message{to = Room, sub_els = [#xdata{type = submit,
+                                                      fields = ApprovalFs}]}),
+    #muc_user{
+       items = [#muc_item{role = participant,
+                         jid = PeerJID,
+                         affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    ct:comment("Waiting for the slave to leave"),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_voice_request_slave(Config) ->
+    Room = muc_room_jid(Config),
+    MyJID = my_jid(Config),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    wait_for_master(Config),
+    {[], _, _} = muc_join(Config, visitor),
+    ct:comment("Requesting voice"),
+    Fs = muc_request:encode([{role, participant}]),
+    X = #xdata{type = submit, fields = Fs},
+    send(Config, #message{to = Room, sub_els = [X]}),
+    ct:comment("Waiting to become a participant"),
+    #muc_user{
+       items = [#muc_item{role = participant,
+                         jid = MyJID,
+                         affiliation = none}]} =
+       recv_muc_presence(Config, MyNickJID, available),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_change_role_master(Config) ->
+    Room = muc_room_jid(Config),
+    MyJID = my_jid(Config),
+    MyNick = ?config(nick, Config),
+    PeerJID = ?config(slave, Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ok = muc_join_new(Config),
+    ct:comment("Waiting for the slave to join"),
+    wait_for_slave(Config),
+    #muc_user{items = [#muc_item{role = participant,
+                                jid = PeerJID,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    lists:foreach(
+      fun(Role) ->
+             ct:comment("Checking if the slave is not in the roles list"),
+             case muc_get_role(Config, Role) of
+                 [#muc_item{jid = MyJID, affiliation = owner,
+                            role = moderator, nick = MyNick}] when Role == moderator ->
+                     ok;
+                 [] ->
+                     ok
+             end,
+             Reason = randoms:get_string(),
+             put_event(Config, {Role, Reason}),
+             ok = muc_set_role(Config, Role, Reason),
+             ct:comment("Receiving role change to ~s", [Role]),
+             #muc_user{
+                items = [#muc_item{role = Role,
+                                   affiliation = none,
+                                   reason = Reason}]} =
+                 recv_muc_presence(Config, PeerNickJID, available),
+             [#muc_item{role = Role, affiliation = none,
+                        nick = PeerNick}|_] = muc_get_role(Config, Role)
+      end, [visitor, participant, moderator]),
+    put_event(Config, disconnect),
+    wait_for_slave(Config),
+    flush(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_change_role_slave(Config) ->
+    wait_for_master(Config),
+    {[], _, _} = muc_join(Config),
+    muc_change_role_slave(Config, get_event(Config)).
+
+muc_change_role_slave(Config, {Role, Reason}) ->
+    Room = muc_room_jid(Config),
+    MyNick = ?config(slave_nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    ct:comment("Receiving role change to ~s", [Role]),
+    #muc_user{status_codes = Codes,
+             items = [#muc_item{role = Role,
+                                affiliation = none,
+                                reason = Reason}]} =
+       recv_muc_presence(Config, MyNickJID, available),
+    true = lists:member(110, Codes),
+    muc_change_role_slave(Config, get_event(Config));
+muc_change_role_slave(Config, disconnect) ->
+    ok = muc_leave(Config),
+    wait_for_master(Config),
+    disconnect(Config).
+
+muc_change_affiliation_master(Config) ->
+    Room = muc_room_jid(Config),
+    MyJID = my_jid(Config),
+    MyBareJID = jid:remove_resource(MyJID),
+    MyNick = ?config(nick, Config),
+    PeerJID = ?config(slave, Config),
+    PeerBareJID = jid:remove_resource(PeerJID),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ok = muc_join_new(Config),
+    ct:comment("Waiting for the slave to join"),
+    wait_for_slave(Config),
+    #muc_user{items = [#muc_item{role = participant,
+                                jid = PeerJID,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    lists:foreach(
+      fun({Aff, Role, Status}) ->
+             ct:comment("Checking if slave is not in affiliation list"),
+             case muc_get_affiliation(Config, Aff) of
+                 [#muc_item{jid = MyBareJID,
+                            affiliation = owner}] when Aff == owner ->
+                     ok;
+                 [] ->
+                     ok
+             end,
+             Reason = randoms:get_string(),
+             put_event(Config, {Aff, Role, Status, Reason}),
+             ok = muc_set_affiliation(Config, Aff, Reason),
+             ct:comment("Receiving affiliation change to ~s", [Aff]),
+             #muc_user{
+                items = [#muc_item{role = Role,
+                                   affiliation = Aff,
+                                   actor = Actor,
+                                   reason = Reason}]} =
+                 recv_muc_presence(Config, PeerNickJID, Status),
+             if Aff == outcast ->
+                     ct:comment("Checking if actor is set"),
+                     #muc_actor{nick = MyNick} = Actor;
+                true ->
+                     ok
+             end,
+             Affs = muc_get_affiliation(Config, Aff),
+             ct:comment("Checking if the affiliation was correctly set"),
+             case lists:keyfind(PeerBareJID, #muc_item.jid, Affs) of
+                 false when Aff == none ->
+                     ok;
+                 #muc_item{affiliation = Aff} ->
+                     ok
+             end
+      end, [{member, participant, available}, {none, participant, available},
+           {admin, moderator, available}, {owner, moderator, available},
+           {outcast, none, unavailable}]),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_change_affiliation_slave(Config) ->
+    wait_for_master(Config),
+    {[], _, _} = muc_join(Config),
+    muc_change_affiliation_slave(Config, get_event(Config)).
+
+muc_change_affiliation_slave(Config, {Aff, Role, Status, Reason}) ->
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(master_nick, Config),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    ct:comment("Receiving affiliation change to ~s", [Aff]),
+    #muc_user{status_codes = Codes,
+             items = [#muc_item{role = Role,
+                                actor = Actor,
+                                affiliation = Aff,
+                                reason = Reason}]} =
+       recv_muc_presence(Config, MyNickJID, Status),
+    true = lists:member(110, Codes),
+    if Aff == outcast ->
+           ct:comment("Checking for status code '301' (banned)"),
+           true = lists:member(301, Codes),
+           ct:comment("Checking if actor is set"),
+           #muc_actor{nick = PeerNick} = Actor,
+           disconnect(Config);
+       true ->
+           muc_change_affiliation_slave(Config, get_event(Config))
+    end.
+
+muc_kick_master(Config) ->
+    Room = muc_room_jid(Config),
+    MyNick = ?config(nick, Config),
+    PeerJID = ?config(slave, Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    Reason = <<"Testing">>,
+    ok = muc_join_new(Config),
+    ct:comment("Waiting for the slave to join"),
+    wait_for_slave(Config),
+    #muc_user{items = [#muc_item{role = participant,
+                                jid = PeerJID,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    [#muc_item{role = participant, affiliation = none,
+              nick = PeerNick}|_] = muc_get_role(Config, participant),
+    ct:comment("Kicking slave"),
+    ok = muc_set_role(Config, none, Reason),
+    ct:comment("Receiving role change to 'none'"),
+    #muc_user{
+       status_codes = Codes,
+       items = [#muc_item{role = none,
+                         affiliation = none,
+                         actor = #muc_actor{nick = MyNick},
+                         reason = Reason}]} =
+       recv_muc_presence(Config, PeerNickJID, unavailable),
+    [] = muc_get_role(Config, participant),
+    ct:comment("Checking if the code is '307' (kicked)"),
+    true = lists:member(307, Codes),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_kick_slave(Config) ->
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(master_nick, Config),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    Reason = <<"Testing">>,
+    wait_for_master(Config),
+    {[], _, _} = muc_join(Config),
+    ct:comment("Receiving role change to 'none'"),
+    #muc_user{status_codes = Codes,
+             items = [#muc_item{role = none,
+                                affiliation = none,
+                                actor = #muc_actor{nick = PeerNick},
+                                reason = Reason}]} =
+       recv_muc_presence(Config, MyNickJID, unavailable),
+    ct:comment("Checking if codes '110' (self-presence) "
+              "and '307' (kicked) are present"),
+    true = lists:member(110, Codes),
+    true = lists:member(307, Codes),
+    disconnect(Config).
+
+muc_destroy_master(Config) ->
+    Reason = <<"Testing">>,
+    Room = muc_room_jid(Config),
+    AltRoom = alt_room_jid(Config),
+    PeerJID = ?config(peer, Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    ok = muc_join_new(Config),
+    ct:comment("Waiting for slave to join"),
+    wait_for_slave(Config),
+    #muc_user{items = [#muc_item{role = participant,
+                                jid = PeerJID,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    wait_for_slave(Config),
+    ok = muc_destroy(Config, Reason),
+    ct:comment("Receiving destruction presence"),
+    #muc_user{items = [#muc_item{role = none,
+                                affiliation = none}],
+             destroy = #muc_destroy{jid = AltRoom,
+                                    reason = Reason}} =
+       recv_muc_presence(Config, MyNickJID, unavailable),
+    disconnect(Config).
+
+muc_destroy_slave(Config) ->
+    Reason = <<"Testing">>,
+    Room = muc_room_jid(Config),
+    AltRoom = alt_room_jid(Config),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    wait_for_master(Config),
+    {[], _, _} = muc_join(Config),
+    #stanza_error{reason = 'forbidden'} = muc_destroy(Config, Reason),
+    wait_for_master(Config),
+    ct:comment("Receiving destruction presence"),
+    #muc_user{items = [#muc_item{role = none,
+                                affiliation = none}],
+             destroy = #muc_destroy{jid = AltRoom,
+                                    reason = Reason}} =
+       recv_muc_presence(Config, MyNickJID, unavailable),
+    disconnect(Config).
+
+muc_vcard_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    FN = randoms:get_string(),
+    VCard = #vcard_temp{fn = FN},
+    ok = muc_join_new(Config),
+    ct:comment("Waiting for slave to join"),
+    wait_for_slave(Config),
+    #muc_user{items = [#muc_item{role = participant,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    #stanza_error{reason = 'item-not-found'} = muc_get_vcard(Config),
+    ok = muc_set_vcard(Config, VCard),
+    VCard = muc_get_vcard(Config),
+    put_event(Config, VCard),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    leave = get_event(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_vcard_slave(Config) ->
+    wait_for_master(Config),
+    {[], _, _} = muc_join(Config),
+    VCard = get_event(Config),
+    VCard = muc_get_vcard(Config),
+    #stanza_error{reason = 'forbidden'} = muc_set_vcard(Config, VCard),
+    ok = muc_leave(Config),
+    VCard = muc_get_vcard(Config),
+    put_event(Config, leave),
+    disconnect(Config).
+
+muc_nick_change_master(Config) ->
+    NewNick = randoms:get_string(),
+    PeerJID = ?config(peer, Config),
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_master_join(Config),
+    put_event(Config, {new_nick, NewNick}),
+    ct:comment("Waiting for nickchange presence from the slave"),
+    #muc_user{status_codes = Codes,
+             items = [#muc_item{jid = PeerJID,
+                                nick = NewNick}]} =
+       recv_muc_presence(Config, PeerNickJID, unavailable),
+    ct:comment("Checking if code '303' (nick change) is set"),
+    true = lists:member(303, Codes),
+    ct:comment("Waiting for updated presence from the slave"),
+    PeerNewNickJID = jid:replace_resource(PeerNickJID, NewNick),
+    recv_muc_presence(Config, PeerNewNickJID, available),
+    ct:comment("Waiting for the slave to leave"),
+    recv_muc_presence(Config, PeerNewNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_nick_change_slave(Config) ->
+    MyJID = my_jid(Config),
+    MyNickJID = my_muc_jid(Config),
+    {[], _, _} = muc_slave_join(Config),
+    {new_nick, NewNick} = get_event(Config),
+    MyNewNickJID = jid:replace_resource(MyNickJID, NewNick),
+    ct:comment("Sending new presence"),
+    send(Config, #presence{to = MyNewNickJID}),
+    ct:comment("Receiving nickchange self-presence"),
+    #muc_user{status_codes = Codes1,
+             items = [#muc_item{role = participant,
+                                jid = MyJID,
+                                nick = NewNick}]} =
+       recv_muc_presence(Config, MyNickJID, unavailable),
+    ct:comment("Checking if codes '110' (self-presence) and "
+              "'303' (nickchange) are present"),
+    lists:member(110, Codes1),
+    lists:member(303, Codes1),
+    ct:comment("Receiving self-presence update"),
+    #muc_user{status_codes = Codes2,
+             items = [#muc_item{jid = MyJID,
+                                role = participant}]} =
+       recv_muc_presence(Config, MyNewNickJID, available),
+    ct:comment("Checking if code '110' (self-presence) is set"),
+    lists:member(110, Codes2),
+    NewConfig = set_opt(nick, NewNick, Config),
+    ok = muc_leave(NewConfig),
+    disconnect(NewConfig).
+
+muc_config_title_desc_master(Config) ->
+    Title = randoms:get_string(),
+    Desc = randoms:get_string(),
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ok = muc_master_join(Config),
+    [104] = muc_set_config(Config, [{roomname, Title}, {roomdesc, Desc}]),
+    RoomCfg = muc_get_config(Config),
+    Title = proplists:get_value(roomname, RoomCfg),
+    Desc = proplists:get_value(roomdesc, RoomCfg),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_title_desc_slave(Config) ->
+    {[], _, _} = muc_slave_join(Config),
+    [104] = muc_recv_config_change_message(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_public_list_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ok = muc_join_new(Config),
+    wait_for_slave(Config),
+    recv_muc_presence(Config, PeerNickJID, available),
+    lists:member(<<"muc_public">>, get_features(Config, Room)),
+    [104] = muc_set_config(Config, [{public_list, false},
+                                   {publicroom, false}]),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    lists:member(<<"muc_hidden">>, get_features(Config, Room)),
+    wait_for_slave(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_public_list_slave(Config) ->
+    Room = muc_room_jid(Config),
+    wait_for_master(Config),
+    PeerNick = ?config(peer_nick, Config),
+    PeerNickJID = peer_muc_jid(Config),
+    [#disco_item{jid = Room}] = muc_disco_items(Config),
+    [#disco_item{jid = PeerNickJID,
+                name = PeerNick}] = muc_disco_room_items(Config),
+    {[], _, _} = muc_join(Config),
+    [104] = muc_recv_config_change_message(Config),
+    ok = muc_leave(Config),
+    [] = muc_disco_items(Config),
+    [] = muc_disco_room_items(Config),
+    wait_for_master(Config),
+    disconnect(Config).
+
+muc_config_password_master(Config) ->
+    Password = randoms:get_string(),
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ok = muc_join_new(Config),
+    lists:member(<<"muc_unsecured">>, get_features(Config, Room)),
+    [104] = muc_set_config(Config, [{passwordprotectedroom, true},
+                                   {roomsecret, Password}]),
+    lists:member(<<"muc_passwordprotected">>, get_features(Config, Room)),
+    put_event(Config, Password),
+    recv_muc_presence(Config, PeerNickJID, available),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_password_slave(Config) ->
+    Password = get_event(Config),
+    #stanza_error{reason = 'not-authorized'} = muc_join(Config),
+    #stanza_error{reason = 'not-authorized'} =
+       muc_join(Config, #muc{password = randoms:get_string()}),
+    {[], _, _} = muc_join(Config, #muc{password = Password}),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_whois_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerNickJID = peer_muc_jid(Config),
+    MyNickJID = my_muc_jid(Config),
+    ok = muc_master_join(Config),
+    lists:member(<<"muc_semianonymous">>, get_features(Config, Room)),
+    [172] = muc_set_config(Config, [{whois, anyone}]),
+    lists:member(<<"muc_nonanonymous">>, get_features(Config, Room)),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    recv_muc_presence(Config, PeerNickJID, available),
+    send(Config, #presence{to = Room}),
+    recv_muc_presence(Config, MyNickJID, available),
+    [173] = muc_set_config(Config, [{whois, moderators}]),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_whois_slave(Config) ->
+    PeerJID = ?config(peer, Config),
+    PeerNickJID = peer_muc_jid(Config),
+    {[], _, _} = muc_slave_join(Config),
+    ct:comment("Checking if the room becomes non-anonymous (code '172')"),
+    [172] = muc_recv_config_change_message(Config),
+    ct:comment("Re-joining in order to check status codes"),
+    ok = muc_leave(Config),
+    {[], _, Codes} = muc_join(Config),
+    ct:comment("Checking if code '100' (non-anonymous) present"),
+    true = lists:member(100, Codes),
+    ct:comment("Receiving presence from peer with JID exposed"),
+    #muc_user{items = [#muc_item{jid = PeerJID}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    ct:comment("Waiting for the room to become anonymous again (code '173')"),
+    [173] = muc_recv_config_change_message(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_members_only_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(peer, Config),
+    PeerBareJID = jid:remove_resource(PeerJID),
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_master_join(Config),
+    lists:member(<<"muc_open">>, get_features(Config, Room)),
+    [104] = muc_set_config(Config, [{membersonly, true}]),
+    #muc_user{status_codes = Codes,
+             items = [#muc_item{jid = PeerJID,
+                                affiliation = none,
+                                role = none}]} =
+       recv_muc_presence(Config, PeerNickJID, unavailable),
+    ct:comment("Checking if code '322' (non-member) is set"),
+    true = lists:member(322, Codes),
+    lists:member(<<"muc_membersonly">>, get_features(Config, Room)),
+    ct:comment("Waiting for slave to fail joining the room"),
+    set_member = get_event(Config),
+    ok = muc_set_affiliation(Config, member, randoms:get_string()),
+    #message{from = Room, type = normal} = Msg = recv_message(Config),
+    #muc_user{items = [#muc_item{jid = PeerBareJID,
+                                affiliation = member}]} =
+       xmpp:get_subtag(Msg, #muc_user{}),
+    ct:comment("Asking peer to join"),
+    put_event(Config, join),
+    ct:comment("Waiting for peer to join"),
+    recv_muc_presence(Config, PeerNickJID, available),
+    ok = muc_set_affiliation(Config, none, randoms:get_string()),
+    ct:comment("Waiting for peer to be kicked"),
+    #muc_user{status_codes = NewCodes,
+             items = [#muc_item{affiliation = none,
+                                role = none}]} =
+       recv_muc_presence(Config, PeerNickJID, unavailable),
+    ct:comment("Checking if code '321' (became non-member in "
+              "members-only room) is set"),
+    true = lists:member(321, NewCodes),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_members_only_slave(Config) ->
+    MyJID = my_jid(Config),
+    MyNickJID = my_muc_jid(Config),
+    {[], _, _} = muc_slave_join(Config),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Getting kicked because the room has become members-only"),
+    #muc_user{status_codes = Codes,
+             items = [#muc_item{jid = MyJID,
+                                role = none,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, MyNickJID, unavailable),
+    ct:comment("Checking if the code '110' (self-presence) "
+              "and '322' (non-member) is set"),
+    true = lists:member(110, Codes),
+    true = lists:member(322, Codes),
+    ct:comment("Fail trying to join members-only room"),
+    #stanza_error{reason = 'registration-required'} = muc_join(Config),
+    ct:comment("Asking the peer to set us member"),
+    put_event(Config, set_member),
+    ct:comment("Waiting for the peer to ask for join"),
+    join = get_event(Config),
+    {[], _, _} = muc_join(Config, participant, member),
+    #muc_user{status_codes = NewCodes,
+             items = [#muc_item{jid = MyJID,
+                                role = none,
+                                affiliation = none}]} =
+       recv_muc_presence(Config, MyNickJID, unavailable),
+    ct:comment("Checking if the code '110' (self-presence) "
+              "and '321' (became non-member in members-only room) is set"),
+    true = lists:member(110, NewCodes),
+    true = lists:member(321, NewCodes),
+    disconnect(Config).
+
+muc_config_moderated_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_master_join(Config),
+    lists:member(<<"muc_moderated">>, get_features(Config, Room)),
+    ok = muc_set_role(Config, visitor, randoms:get_string()),
+    #muc_user{items = [#muc_item{role = visitor}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    set_unmoderated = get_event(Config),
+    [104] = muc_set_config(Config, [{moderatedroom, false}]),
+    #message{from = PeerNickJID, type = groupchat} = recv_message(Config),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    lists:member(<<"muc_unmoderated">>, get_features(Config, Room)),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_moderated_slave(Config) ->
+    Room = muc_room_jid(Config),
+    MyNickJID = my_muc_jid(Config),
+    {[], _, _} = muc_slave_join(Config),
+    #muc_user{items = [#muc_item{role = visitor}]} =
+       recv_muc_presence(Config, MyNickJID, available),
+    send(Config, #message{to = Room, type = groupchat}),
+    ErrMsg = #message{from = Room, type = error} = recv_message(Config),
+    #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg),
+    put_event(Config, set_unmoderated),
+    [104] = muc_recv_config_change_message(Config),
+    send(Config, #message{to = Room, type = groupchat}),
+    #message{from = MyNickJID, type = groupchat} = recv_message(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_private_messages_master(Config) ->
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_master_join(Config),
+    ct:comment("Waiting for a private message from the slave"),
+    #message{from = PeerNickJID, type = chat} = recv_message(Config),
+    ok = muc_set_role(Config, visitor, <<>>),
+    ct:comment("Waiting for the peer to become a visitor"),
+    recv_muc_presence(Config, PeerNickJID, available),
+    ct:comment("Waiting for a private message from the slave"),
+    #message{from = PeerNickJID, type = chat} = recv_message(Config),
+    [104] = muc_set_config(Config, [{allow_private_messages_from_visitors, moderators}]),
+    ct:comment("Waiting for a private message from the slave"),
+    #message{from = PeerNickJID, type = chat} = recv_message(Config),
+    [104] = muc_set_config(Config, [{allow_private_messages_from_visitors, nobody}]),
+    wait_for_slave(Config),
+    [104] = muc_set_config(Config, [{allow_private_messages_from_visitors, anyone},
+                                   {allow_private_messages, false}]),
+    ct:comment("Fail trying to send a private message"),
+    send(Config, #message{to = PeerNickJID, type = chat}),
+    #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config),
+    #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg),
+    ok = muc_set_role(Config, participant, <<>>),
+    ct:comment("Waiting for the peer to become a participant"),
+    recv_muc_presence(Config, PeerNickJID, available),
+    ct:comment("Waiting for the peer to leave"),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_private_messages_slave(Config) ->
+    MyNickJID = my_muc_jid(Config),
+    PeerNickJID = peer_muc_jid(Config),
+    {[], _, _} = muc_slave_join(Config),
+    ct:comment("Sending a private message"),
+    send(Config, #message{to = PeerNickJID, type = chat}),
+    ct:comment("Waiting to become a visitor"),
+    #muc_user{items = [#muc_item{role = visitor}]} =
+       recv_muc_presence(Config, MyNickJID, available),
+    ct:comment("Sending a private message"),
+    send(Config, #message{to = PeerNickJID, type = chat}),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Sending a private message"),
+    send(Config, #message{to = PeerNickJID, type = chat}),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Fail trying to send a private message"),
+    send(Config, #message{to = PeerNickJID, type = chat}),
+    #message{from = PeerNickJID, type = error} = ErrMsg1 = recv_message(Config),
+    #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg1),
+    wait_for_master(Config),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Waiting to become a participant again"),
+    #muc_user{items = [#muc_item{role = participant}]} =
+       recv_muc_presence(Config, MyNickJID, available),
+    ct:comment("Fail trying to send a private message"),
+    send(Config, #message{to = PeerNickJID, type = chat}),
+    #message{from = PeerNickJID, type = error} = ErrMsg2 = recv_message(Config),
+    #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg2),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_query_master(Config) ->
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_join_new(Config),
+    wait_for_slave(Config),
+    recv_muc_presence(Config, PeerNickJID, available),
+    ct:comment("Receiving IQ query from the slave"),
+    #iq{type = get, from = PeerNickJID, id = I,
+       sub_els = [#ping{}]} = recv_iq(Config),
+    send(Config, #iq{type = result, to = PeerNickJID, id = I}),
+    [104] = muc_set_config(Config, [{allow_query_users, false}]),
+    ct:comment("Fail trying to send IQ"),
+    #iq{type = error, from = PeerNickJID} = Err =
+       send_recv(Config, #iq{type = get, to = PeerNickJID,
+                             sub_els = [#ping{}]}),
+    #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_query_slave(Config) ->
+    PeerNickJID = peer_muc_jid(Config),
+    wait_for_master(Config),
+    ct:comment("Checking if IQ queries are denied from non-occupants"),
+    #iq{type = error, from = PeerNickJID} = Err1 =
+       send_recv(Config, #iq{type = get, to = PeerNickJID,
+                             sub_els = [#ping{}]}),
+    #stanza_error{reason = 'not-acceptable'} = xmpp:get_error(Err1),
+    {[], _, _} = muc_join(Config),
+    ct:comment("Sending IQ to the master"),
+    #iq{type = result, from = PeerNickJID, sub_els = []} =
+       send_recv(Config, #iq{to = PeerNickJID, type = get, sub_els = [#ping{}]}),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Fail trying to send IQ"),
+    #iq{type = error, from = PeerNickJID} = Err2 =
+       send_recv(Config, #iq{type = get, to = PeerNickJID,
+                             sub_els = [#ping{}]}),
+    #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err2),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_allow_invites_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(peer, Config),
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_master_join(Config),
+    [104] = muc_set_config(Config, [{allowinvites, true}]),
+    ct:comment("Receiving an invitation from the slave"),
+    #message{from = Room, type = normal} = recv_message(Config),
+    [104] = muc_set_config(Config, [{allowinvites, false}]),
+    send_invitation = get_event(Config),
+    ct:comment("Sending an invitation"),
+    send(Config, #message{to = Room, type = normal,
+                         sub_els =
+                             [#muc_user{
+                                 invites =
+                                     [#muc_invite{to = PeerJID}]}]}),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_allow_invites_slave(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(peer, Config),
+    InviteMsg = #message{to = Room, type = normal,
+                        sub_els =
+                            [#muc_user{
+                                invites =
+                                    [#muc_invite{to = PeerJID}]}]},
+    {[], _, _} = muc_slave_join(Config),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Sending an invitation"),
+    send(Config, InviteMsg),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Fail sending an invitation"),
+    send(Config, InviteMsg),
+    #message{from = Room, type = error} = Err = recv_message(Config),
+    #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err),
+    ct:comment("Checking if the master is still able to send invitations"),
+    put_event(Config, send_invitation),
+    #message{from = Room, type = normal} = recv_message(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_visitor_status_master(Config) ->
+    PeerNickJID = peer_muc_jid(Config),
+    Status = xmpp:mk_text(randoms:get_string()),
+    ok = muc_join_new(Config),
+    [104] = muc_set_config(Config, [{members_by_default, false}]),
+    ct:comment("Asking the slave to join as a visitor"),
+    put_event(Config, {join, Status}),
+    #muc_user{items = [#muc_item{role = visitor}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    ct:comment("Receiving status change from the visitor"),
+    #presence{from = PeerNickJID, status = Status} = recv_presence(Config),
+    [104] = muc_set_config(Config, [{allow_visitor_status, false}]),
+    ct:comment("Receiving status change with <status/> stripped"),
+    #presence{from = PeerNickJID, status = []} = recv_presence(Config),
+    ct:comment("Waiting for the slave to leave"),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_visitor_status_slave(Config) ->
+    Room = muc_room_jid(Config),
+    MyNickJID = my_muc_jid(Config),
+    ct:comment("Waiting for 'join' command from the master"),
+    {join, Status} = get_event(Config),
+    {[], _, _} = muc_join(Config, visitor, none),
+    ct:comment("Sending status change"),
+    send(Config, #presence{to = Room, status = Status}),
+    #presence{from = MyNickJID, status = Status} = recv_presence(Config),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Sending status change again"),
+    send(Config, #presence{to = Room, status = Status}),
+    #presence{from = MyNickJID, status = []} = recv_presence(Config),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_allow_voice_requests_master(Config) ->
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_join_new(Config),
+    [104] = muc_set_config(Config, [{members_by_default, false}]),
+    ct:comment("Asking the slave to join as a visitor"),
+    put_event(Config, join),
+    #muc_user{items = [#muc_item{role = visitor}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    [104] = muc_set_config(Config, [{allow_voice_requests, false}]),
+    ct:comment("Waiting for the slave to leave"),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_allow_voice_requests_slave(Config) ->
+    Room = muc_room_jid(Config),
+    ct:comment("Waiting for 'join' command from the master"),
+    join = get_event(Config),
+    {[], _, _} = muc_join(Config, visitor),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Fail sending voice request"),
+    Fs = muc_request:encode([{role, participant}]),
+    X = #xdata{type = submit, fields = Fs},
+    send(Config, #message{to = Room, sub_els = [X]}),
+    #message{from = Room, type = error} = Err = recv_message(Config),
+    #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_voice_request_interval_master(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(peer, Config),
+    PeerNick = ?config(peer_nick, Config),
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_join_new(Config),
+    [104] = muc_set_config(Config, [{members_by_default, false}]),
+    ct:comment("Asking the slave to join as a visitor"),
+    put_event(Config, join),
+    #muc_user{items = [#muc_item{role = visitor}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    [104] = muc_set_config(Config, [{voice_request_min_interval, 5}]),
+    ct:comment("Receiving a voice request from slave"),
+    #message{from = Room, type = normal} = recv_message(Config),
+    ct:comment("Deny voice request at first"),
+    Fs = muc_request:encode([{jid, PeerJID}, {role, participant},
+                            {nick, PeerNick}, {request_allow, false}]),
+    send(Config, #message{to = Room, sub_els = [#xdata{type = submit,
+                                                       fields = Fs}]}),
+    put_event(Config, denied),
+    ct:comment("Waiting for repeated voice request from the slave"),
+    #message{from = Room, type = normal} = recv_message(Config),
+    ct:comment("Waiting for the slave to leave"),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_voice_request_interval_slave(Config) ->
+    Room = muc_room_jid(Config),
+    Fs = muc_request:encode([{role, participant}]),
+    X = #xdata{type = submit, fields = Fs},
+    ct:comment("Waiting for 'join' command from the master"),
+    join = get_event(Config),
+    {[], _, _} = muc_join(Config, visitor),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Sending voice request"),
+    send(Config, #message{to = Room, sub_els = [X]}),
+    ct:comment("Waiting for the master to deny our voice request"),
+    denied = get_event(Config),
+    ct:comment("Requesting voice again"),
+    send(Config, #message{to = Room, sub_els = [X]}),
+    ct:comment("Receving voice request error because we're sending to fast"),
+    #message{from = Room, type = error} = Err = recv_message(Config),
+    #stanza_error{reason = 'resource-constraint'} = xmpp:get_error(Err),
+    ct:comment("Waiting for 5 seconds"),
+    timer:sleep(timer:seconds(5)),
+    ct:comment("Repeating again"),
+    send(Config, #message{to = Room, sub_els = [X]}),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_visitor_nickchange_master(Config) ->
+    PeerNickJID = peer_muc_jid(Config),
+    ok = muc_join_new(Config),
+    [104] = muc_set_config(Config, [{members_by_default, false}]),
+    ct:comment("Asking the slave to join as a visitor"),
+    put_event(Config, join),
+    ct:comment("Waiting for the slave to join"),
+    #muc_user{items = [#muc_item{role = visitor}]} =
+       recv_muc_presence(Config, PeerNickJID, available),
+    [104] = muc_set_config(Config, [{allow_visitor_nickchange, false}]),
+    ct:comment("Waiting for the slave to leave"),
+    recv_muc_presence(Config, PeerNickJID, unavailable),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_config_visitor_nickchange_slave(Config) ->
+    NewNick = randoms:get_string(),
+    MyNickJID = my_muc_jid(Config),
+    MyNewNickJID = jid:replace_resource(MyNickJID, NewNick),
+    ct:comment("Waiting for 'join' command from the master"),
+    join = get_event(Config),
+    {[], _, _} = muc_join(Config, visitor),
+    [104] = muc_recv_config_change_message(Config),
+    ct:comment("Fail trying to change nickname"),
+    send(Config, #presence{to = MyNewNickJID}),
+    #presence{from = MyNewNickJID, type = error} = Err = recv_presence(Config),
+    #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err),
+    ok = muc_leave(Config),
+    disconnect(Config).
+
+muc_register_master(Config) ->
+    MUC = muc_jid(Config),
+    %% Register nick "master1"
+    muc_register_nick(Config, MUC, <<"">>, <<"master1">>),
+    %% Unregister nick "master1" via jabber:register
+    #iq{type = result, sub_els = []} =
+       send_recv(Config, #iq{type = set, to = MUC,
+                             sub_els = [#register{remove = true}]}),
+    %% Register nick "master2"
+    muc_register_nick(Config, MUC, <<"">>, <<"master2">>),
+    %% Now register nick "master"
+    muc_register_nick(Config, MUC, <<"master2">>, <<"master">>),
+    %% Wait for slave to fail trying to register nick "master"
+    wait_for_slave(Config),
+    wait_for_slave(Config),
+    %% Now register empty ("") nick, which means we're unregistering
+    muc_register_nick(Config, MUC, <<"master">>, <<"">>),
+    disconnect(Config).
+
+muc_register_slave(Config) ->
+    MUC = muc_jid(Config),
+    wait_for_master(Config),
+    %% Trying to register occupied nick "master"
+    Fs = muc_register:encode([{roomnick, <<"master">>}]),
+    X = #xdata{type = submit, fields = Fs},
+    #iq{type = error} =
+       send_recv(Config, #iq{type = set, to = MUC,
+                             sub_els = [#register{xdata = X}]}),
+    wait_for_master(Config),
+    disconnect(Config).
+
+%%%===================================================================
+%%% Internal functions
+%%%===================================================================
+master_slave_test(T) ->
+    {T, [parallel], [list_to_atom(atom_to_list(T) ++ "_master"),
+                    list_to_atom(atom_to_list(T) ++ "_slave")]}.
+
+recv_muc_presence(Config, From, Type) ->
+    Pres = #presence{from = From, type = Type} = recv_presence(Config),
+    xmpp:get_subtag(Pres, #muc_user{}).
+
+muc_join_new(Config) ->
+    muc_join_new(Config, muc_room_jid(Config)).
+
+muc_join_new(Config, Room) ->
+    MyJID = my_jid(Config),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    ct:comment("Joining new room"),
+    send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}),
+    %% As per XEP-0045 we MUST receive stanzas in the following order:
+    %% 1. In-room presence from other occupants
+    %% 2. In-room presence from the joining entity itself (so-called "self-presence")
+    %% 3. Room history (if any)
+    %% 4. The room subject
+    %% 5. Live messages, presence updates, new user joins, etc.
+    %% As this is the newly created room, we receive only the 2nd and 4th stanza.
+    #muc_user{
+       status_codes = Codes,
+       items = [#muc_item{role = moderator,
+                         jid = MyJID,
+                         affiliation = owner}]} =
+       xmpp:get_subtag(?recv1(#presence{from = MyNickJID}), #muc_user{}),
+    ct:comment("Checking if codes '110' (self-presence) and "
+              "'201' (new room) is set"),
+    true = lists:member(110, Codes),
+    true = lists:member(201, Codes),
+    ct:comment("Receiving empty room subject"),
+    #message{from = Room, type = groupchat, body = [],
+            subject = [#text{data = <<>>}]} = recv_message(Config),
+    case ?config(persistent_room, Config) of
+       true ->
+           [104] = muc_set_config(Config, [{persistentroom, true}], Room),
+           ok;
+       false ->
+           ok
+    end.
+
+muc_recv_history_and_subject(Config) ->
+    ct:comment("Receiving room history and/or subject"),
+    muc_recv_history_and_subject(Config, []).
+
+muc_recv_history_and_subject(Config, History) ->
+    Room = muc_room_jid(Config),
+    #message{type = groupchat, subject = Subj,
+            body = Body, thread = Thread} = Msg = recv_message(Config),
+    case xmpp:get_subtag(Msg, #delay{}) of
+       #delay{from = Room} ->
+           muc_recv_history_and_subject(Config, [Msg|History]);
+       false when Subj /= [], Body == [], Thread == undefined ->
+           {lists:reverse(History), Msg}
+    end.
+
+muc_join(Config) ->
+    muc_join(Config, participant, none, #muc{}).
+
+muc_join(Config, Role) when is_atom(Role) ->
+    muc_join(Config, Role, none, #muc{});
+muc_join(Config, #muc{} = SubEl) ->
+    muc_join(Config, participant, none, SubEl).
+
+muc_join(Config, Role, Aff) when is_atom(Role), is_atom(Aff) ->
+    muc_join(Config, Role, Aff, #muc{});
+muc_join(Config, Role, #muc{} = SubEl) when is_atom(Role) ->
+    muc_join(Config, Role, none, SubEl).
+
+muc_join(Config, Role, Aff, SubEl) ->
+    ct:comment("Joining existing room as ~s/~s", [Aff, Role]),
+    MyJID = my_jid(Config),
+    Room = muc_room_jid(Config),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    PeerNick = ?config(peer_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    send(Config, #presence{to = MyNickJID, sub_els = [SubEl]}),
+    case recv_presence(Config) of
+       #presence{type = error, from = MyNickJID} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{});
+       #presence{type = available, from = PeerNickJID} = Pres ->
+           #muc_user{items = [#muc_item{role = moderator,
+                                        affiliation = owner}]} =
+               xmpp:get_subtag(Pres, #muc_user{}),
+           ct:comment("Receiving initial self-presence"),
+           #muc_user{status_codes = Codes,
+                     items = [#muc_item{role = Role,
+                                        jid = MyJID,
+                                        affiliation = Aff}]} =
+               recv_muc_presence(Config, MyNickJID, available),
+           ct:comment("Checking if code '110' (self-presence) is set"),
+           true = lists:member(110, Codes),
+           {History, Subj} = muc_recv_history_and_subject(Config),
+           {History, Subj, Codes};
+       #presence{type = available, from = MyNickJID} = Pres ->
+           #muc_user{status_codes = Codes,
+                     items = [#muc_item{role = Role,
+                                        jid = MyJID,
+                                        affiliation = Aff}]} =
+               xmpp:get_subtag(Pres, #muc_user{}),
+           ct:comment("Checking if code '110' (self-presence) is set"),
+           true = lists:member(110, Codes),
+           {History, Subj} = muc_recv_history_and_subject(Config),
+           {empty, History, Subj, Codes}
+    end.
+
+muc_leave(Config) ->
+    muc_leave(Config, muc_room_jid(Config)).
+
+muc_leave(Config, Room) ->
+    MyJID = my_jid(Config),
+    MyNick = ?config(nick, Config),
+    MyNickJID = jid:replace_resource(Room, MyNick),
+    Mode = ?config(mode, Config),
+    IsPersistent = ?config(persistent_room, Config),
+    if Mode /= slave, IsPersistent ->
+           [104] = muc_set_config(Config, [{persistentroom, false}], Room);
+       true ->
+           ok
+    end,
+    ct:comment("Leaving the room"),
+    send(Config, #presence{to = MyNickJID, type = unavailable}),
+    #muc_user{
+       status_codes = Codes,
+       items = [#muc_item{role = none, jid = MyJID}]} =
+       xmpp:get_subtag(?recv1(#presence{from = MyNickJID,
+                                        type = unavailable}), #muc_user{}),
+    ct:comment("Checking if code '110' (self-presence) is set"),
+    true = lists:member(110, Codes),
+    ok.
+
+muc_get_config(Config) ->
+    ct:comment("Get room config"),
+    Room = muc_room_jid(Config),
+    case send_recv(Config,
+                  #iq{type = get, to = Room,
+                      sub_els = [#muc_owner{}]}) of
+       #iq{type = result,
+           sub_els = [#muc_owner{config = #xdata{type = form} = X}]} ->
+           muc_roomconfig:decode(X#xdata.fields);
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_set_config(Config, RoomConfig) ->
+    muc_set_config(Config, RoomConfig, muc_room_jid(Config)).
+
+muc_set_config(Config, RoomConfig, Room) ->
+    ct:comment("Set room config: ~p", [RoomConfig]),
+    Fs = case RoomConfig of
+            [] -> [];
+            _ -> muc_roomconfig:encode(RoomConfig)
+        end,
+    case send_recv(Config,
+                  #iq{type = set, to = Room,
+                      sub_els = [#muc_owner{config = #xdata{type = submit,
+                                                            fields = Fs}}]}) of
+       #iq{type = result, sub_els = []} ->
+           #message{from = Room, type = groupchat} = Msg = recv_message(Config),
+           #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}),
+           lists:sort(Codes);
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_create_persistent(Config) ->
+    [_|_] = muc_get_config(Config),
+    [] = muc_set_config(Config, [{persistentroom, true}], false),
+    ok.
+
+muc_destroy(Config) ->
+    muc_destroy(Config, <<>>).
+
+muc_destroy(Config, Reason) ->
+    Room = muc_room_jid(Config),
+    AltRoom = alt_room_jid(Config),
+    ct:comment("Destroying a room"),
+    case send_recv(Config,
+                  #iq{type = set, to = Room,
+                      sub_els = [#muc_owner{destroy = #muc_destroy{
+                                                         reason = Reason,
+                                                         jid = AltRoom}}]}) of
+       #iq{type = result, sub_els = []} ->
+           ok;
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_disco_items(Config) ->
+    MUC = muc_jid(Config),
+    ct:comment("Performing disco#items request to ~s", [jid:to_string(MUC)]),
+    #iq{type = result, from = MUC, sub_els = [DiscoItems]} =
+       send_recv(Config, #iq{type = get, to = MUC,
+                             sub_els = [#disco_items{}]}),
+    lists:keysort(#disco_item.jid, DiscoItems#disco_items.items).
+
+muc_disco_room_items(Config) ->
+    Room = muc_room_jid(Config),
+    #iq{type = result, from = Room, sub_els = [DiscoItems]} =
+       send_recv(Config, #iq{type = get, to = Room,
+                             sub_els = [#disco_items{}]}),
+    DiscoItems#disco_items.items.
+
+muc_get_affiliations(Config, Aff) ->
+    Room = muc_room_jid(Config),
+    case send_recv(Config,
+                  #iq{type = get, to = Room,
+                      sub_els = [#muc_admin{items = [#muc_item{affiliation = Aff}]}]}) of
+       #iq{type = result, sub_els = [#muc_admin{items = Items}]} ->
+           Items;
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_master_join(Config) ->
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(slave, Config),
+    PeerNick = ?config(slave_nick, Config),
+    PeerNickJID = jid:replace_resource(Room, PeerNick),
+    ok = muc_join_new(Config),
+    wait_for_slave(Config),
+    #muc_user{items = [#muc_item{jid = PeerJID,
+                                role = participant,
+                                affiliation = none}]} = 
+       recv_muc_presence(Config, PeerNickJID, available),
+    ok.
+
+muc_slave_join(Config) ->
+    wait_for_master(Config),
+    muc_join(Config).
+
+muc_set_role(Config, Role, Reason) ->
+    ct:comment("Changing role to ~s", [Role]),
+    Room = muc_room_jid(Config),
+    PeerNick = ?config(slave_nick, Config),
+    case send_recv(
+          Config,
+          #iq{type = set, to = Room,
+              sub_els =
+                  [#muc_admin{
+                      items = [#muc_item{role = Role,
+                                         reason = Reason,
+                                         nick = PeerNick}]}]}) of
+       #iq{type = result, sub_els = []} ->
+           ok;
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_get_role(Config, Role) ->
+    ct:comment("Requesting list for role '~s'", [Role]),
+    Room = muc_room_jid(Config),
+    case send_recv(
+          Config,
+          #iq{type = get, to = Room,
+              sub_els = [#muc_admin{
+                            items = [#muc_item{role = Role}]}]}) of
+       #iq{type = result, sub_els = [#muc_admin{items = Items}]} ->
+           lists:keysort(#muc_item.affiliation, Items);
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_set_affiliation(Config, Aff, Reason) ->
+    ct:comment("Changing affiliation to ~s", [Aff]),
+    Room = muc_room_jid(Config),
+    PeerJID = ?config(slave, Config),
+    PeerBareJID = jid:remove_resource(PeerJID),
+    case send_recv(
+          Config,
+          #iq{type = set, to = Room,
+              sub_els =
+                  [#muc_admin{
+                      items = [#muc_item{affiliation = Aff,
+                                         reason = Reason,
+                                         jid = PeerBareJID}]}]}) of
+       #iq{type = result, sub_els = []} ->
+           ok;
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_get_affiliation(Config, Aff) ->
+    ct:comment("Requesting list for affiliation '~s'", [Aff]),
+    Room = muc_room_jid(Config),
+    case send_recv(
+          Config,
+          #iq{type = get, to = Room,
+              sub_els = [#muc_admin{
+                            items = [#muc_item{affiliation = Aff}]}]}) of
+       #iq{type = result, sub_els = [#muc_admin{items = Items}]} ->
+           Items;
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_set_vcard(Config, VCard) ->
+    Room = muc_room_jid(Config),
+    ct:comment("Setting vCard for ~s", [jid:to_string(Room)]),
+    case send_recv(Config, #iq{type = set, to = Room,
+                              sub_els = [VCard]}) of
+       #iq{type = result, sub_els = []} ->
+           ok;
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_get_vcard(Config) ->
+    Room = muc_room_jid(Config),
+    ct:comment("Retreiving vCard from ~s", [jid:to_string(Room)]),
+    case send_recv(Config, #iq{type = get, to = Room,
+                              sub_els = [#vcard_temp{}]}) of
+       #iq{type = result, sub_els = [VCard]} ->
+           VCard;
+       #iq{type = error} = Err ->
+           xmpp:get_subtag(Err, #stanza_error{})
+    end.
+
+muc_recv_config_change_message(Config) ->
+    ct:comment("Receiving configuration change notification message"),
+    Room = muc_room_jid(Config),
+    #message{type = groupchat, from = Room} = Msg = recv_message(Config),
+    #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}),
+    lists:sort(Codes).
+
+muc_register_nick(Config, MUC, PrevNick, Nick) ->
+    PrevRegistered = if PrevNick /= <<"">> -> true;
+                       true -> false
+                    end,
+    NewRegistered = if Nick /= <<"">> -> true;
+                      true -> false
+                   end,
+    ct:comment("Requesting registration form"),
+    #iq{type = result,
+       sub_els = [#register{registered = PrevRegistered,
+                            xdata = #xdata{type = form,
+                                           fields = FsWithoutNick}}]} =
+       send_recv(Config, #iq{type = get, to = MUC,
+                             sub_els = [#register{}]}),
+    ct:comment("Checking if previous nick is registered"),
+    PrevNick = proplists:get_value(
+                roomnick, muc_register:decode(FsWithoutNick)),
+    X = #xdata{type = submit, fields = muc_register:encode([{roomnick, Nick}])},
+    ct:comment("Submitting registration form"),
+    #iq{type = result, sub_els = []} =
+       send_recv(Config, #iq{type = set, to = MUC,
+                             sub_els = [#register{xdata = X}]}),
+    ct:comment("Checking if new nick was registered"),
+    #iq{type = result,
+       sub_els = [#register{registered = NewRegistered,
+                            xdata = #xdata{type = form,
+                                           fields = FsWithNick}}]} =
+       send_recv(Config, #iq{type = get, to = MUC,
+                             sub_els = [#register{}]}),
+    Nick = proplists:get_value(
+            roomnick, muc_register:decode(FsWithNick)).
+
+muc_subscribe(Config, Events, Room) ->
+    MyNick = ?config(nick, Config),
+    case send_recv(Config,
+                  #iq{type = set, to = Room,
+                      sub_els = [#muc_subscribe{nick = MyNick,
+                                                events = Events}]}) of
+       #iq{type = result, sub_els = [#muc_subscribe{events = ResEvents}]} ->
+           lists:sort(ResEvents);
+       #iq{type = error} = Err ->
+           xmpp:get_error(Err)
+    end.
+
+muc_unsubscribe(Config, Room) ->
+    case send_recv(Config, #iq{type = set, to = Room,
+                              sub_els = [#muc_unsubscribe{}]}) of
+       #iq{type = result, sub_els = []} ->
+           ok;
+       #iq{type = error} = Err ->
+           xmpp:get_error(Err)
+    end.
index ed1cbd83d3f9da24f42e5f0d4cf6a0ca1fd55b33..7a823844bafcaaa0d6c365cb9c9570720d1bb33d 100644 (file)
@@ -70,10 +70,12 @@ init_config(Config) ->
      {s2s_port, ct:get_config(s2s_port, 5269)},
      {server, ?COMMON_VHOST},
      {user, <<"test_single!#$%^*()`~+-;_=[]{}|\\">>},
+     {nick, <<"nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>},
      {master_nick, <<"master_nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>},
      {slave_nick, <<"slave_nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>},
      {room_subject, <<"hello, world!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>},
      {certfile, CertFile},
+     {persistent_room, true},
      {anonymous, false},
      {type, client},
      {xmlns, ?NS_CLIENT},
@@ -210,6 +212,7 @@ process_stream_features(Config) ->
       end, set_opt(mechs, Mechs, Config), Fs).
 
 disconnect(Config) ->
+    ct:comment("Disconnecting"),
     Socket = ?config(socket, Config),
     try
        ok = send_text(Config, ?STREAM_TRAILER)
@@ -435,22 +438,50 @@ match_failure(Received, Matches) ->
 recv(Config) ->
     receive
         {'$gen_event', {xmlstreamelement, El}} ->
-           NS = case ?config(type, Config) of
-                    client -> ?NS_CLIENT;
-                    server -> ?NS_SERVER;
-                    component -> ?NS_COMPONENT
-                end,
-           decode(El, NS, []);
+           decode_stream_element(Config, El);
        {'$gen_event', {xmlstreamstart, Name, Attrs}} ->
            decode(#xmlel{name = Name, attrs = Attrs}, <<>>, []);
        {'$gen_event', Event} ->
             Event
     end.
 
+recv_iq(Config) ->
+    receive
+       {'$gen_event', {xmlstreamelement, #xmlel{name = <<"iq">>} = El}} ->
+           decode_stream_element(Config, El)
+    end.
+
+recv_presence(Config) ->
+    receive
+       {'$gen_event', {xmlstreamelement, #xmlel{name = <<"presence">>} = El}} ->
+           decode_stream_element(Config, El)
+    end.
+
+recv_message(Config) ->
+    receive
+       {'$gen_event', {xmlstreamelement, #xmlel{name = <<"message">>} = El}} ->
+           decode_stream_element(Config, El)
+    end.
+
+decode_stream_element(Config, El) ->
+    NS = case ?config(type, Config) of
+            client -> ?NS_CLIENT;
+            server -> ?NS_SERVER;
+            component -> ?NS_COMPONENT
+        end,
+    decode(El, NS, []).
+
+format_element(El) ->
+    case erlang:function_exported(ct, log, 5) of
+       true -> ejabberd_web_admin:pretty_print_xml(El);
+       false -> io_lib:format(" ~s~n", El)
+    end.
+
 decode(El, NS, Opts) ->
     try
        Pkt = xmpp:decode(El, NS, Opts),
-       ct:pal("recv: ~p ->~n~s", [El, xmpp:pp(Pkt)]),
+       ct:pal("RECV:~n~s~n~s",
+              [format_element(El), xmpp:pp(Pkt)]),
        Pkt
     catch _:{xmpp_codec, Why} ->
            ct:fail("recv failed: ~p->~n~s",
@@ -475,7 +506,8 @@ send(State, Pkt) ->
                               {undefined, Pkt}
                       end,
     El = xmpp:encode(NewPkt),
-    ct:pal("sent: ~p <-~n~s", [El, xmpp:pp(NewPkt)]),
+    ct:pal("SENT:~n~s~n~s",
+          [format_element(El), xmpp:pp(NewPkt)]),
     Data = case NewPkt of
               #stream_start{} -> fxml:element_to_header(El);
               _ -> fxml:element_to_binary(El)
@@ -483,9 +515,15 @@ send(State, Pkt) ->
     ok = send_text(State, Data),
     NewID.
 
-send_recv(State, IQ) ->
+send_recv(State, #message{} = Msg) ->
+    ID = send(State, Msg),
+    #message{id = ID} = recv_message(State);
+send_recv(State, #presence{} = Pres) ->
+    ID = send(State, Pres),
+    #presence{id = ID} = recv_presence(State);
+send_recv(State, #iq{} = IQ) ->
     ID = send(State, IQ),
-    #iq{id = ID} = recv(State).
+    #iq{id = ID} = recv_iq(State).
 
 sasl_new(<<"PLAIN">>, User, Server, Password) ->
     {<<User/binary, $@, Server/binary, 0, User/binary, 0, Password/binary>>,
@@ -590,6 +628,20 @@ muc_room_jid(Config) ->
     Server = ?config(server, Config),
     jid:make(<<"test">>, <<"conference.", Server/binary>>, <<>>).
 
+my_muc_jid(Config) ->
+    Nick = ?config(nick, Config),
+    RoomJID = muc_room_jid(Config),
+    jid:replace_resource(RoomJID, Nick).
+
+peer_muc_jid(Config) ->
+    PeerNick = ?config(peer_nick, Config),
+    RoomJID = muc_room_jid(Config),
+    jid:replace_resource(RoomJID, PeerNick).
+
+alt_room_jid(Config) ->
+    Server = ?config(server, Config),
+    jid:make(<<"alt">>, <<"conference.", Server/binary>>, <<>>).
+
 mix_jid(Config) ->
     Server = ?config(server, Config),
     jid:make(<<>>, <<"mix.", Server/binary>>, <<>>).
@@ -610,6 +662,7 @@ get_features(Config) ->
     get_features(Config, server_jid(Config)).
 
 get_features(Config, To) ->
+    ct:comment("Getting features of ~s", [jid:to_string(To)]),
     #iq{type = result, sub_els = [#disco_info{features = Features}]} =
         send_recv(Config, #iq{type = get, sub_els = [#disco_info{}], to = To}),
     Features.
@@ -707,3 +760,10 @@ get_event(Config) ->
         {event, Event, Relay} ->
             Event
     end.
+
+flush(Config) ->
+    flush(Config, []).
+
+flush(Config, Msgs) ->
+    receive Msg -> flush(Config, [Msg|Msgs])
+    after 1000 -> lists:reverse(Msgs) end.