From 67720c77137da2e80907f67dccc96fcbdb44c3bf Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Mon, 17 Oct 2016 13:37:23 +0300 Subject: [PATCH] Add more MUC tests --- src/gen_iq_handler.erl | 2 +- src/mod_mam.erl | 34 +- src/mod_muc.erl | 56 +- src/mod_muc_room.erl | 550 ++++---- src/str.erl | 6 + test/ejabberd_SUITE.erl | 581 +++----- test/ejabberd_SUITE_data/ejabberd.yml | 1 + test/muc_tests.erl | 1877 +++++++++++++++++++++++++ test/suite.erl | 80 +- 9 files changed, 2460 insertions(+), 727 deletions(-) create mode 100644 test/muc_tests.erl diff --git a/src/gen_iq_handler.erl b/src/gen_iq_handler.erl index b8a44c96c..8af2cb028 100644 --- a/src/gen_iq_handler.erl +++ b/src/gen_iq_handler.erl @@ -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]}) diff --git a/src/mod_mam.erl b/src/mod_mam.erl index 1daae5aa2..cbd23ebde 100644 --- a/src/mod_mam.erl +++ b/src/mod_mam.erl @@ -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) -> diff --git a/src/mod_muc.erl b/src/mod_muc.erl index 6b71d1e1d..9c17643b8 100644 --- a/src/mod_muc.erl +++ b/src/mod_muc.erl @@ -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 = <>}); 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; (_) -> diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 52401f835..ce6851bc5 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -76,11 +76,6 @@ -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 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 +-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 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 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}. + 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(), 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))). +-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 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) -> diff --git a/src/str.erl b/src/str.erl index 27d21075a..439ae6a7a 100644 --- a/src/str.erl +++ b/src/str.erl @@ -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 %%%=================================================================== diff --git a/test/ejabberd_SUITE.erl b/test/ejabberd_SUITE.erl index b4249bbdf..1a5c89076 100644 --- a/test/ejabberd_SUITE.erl +++ b/test/ejabberd_SUITE.erl @@ -10,23 +10,23 @@ -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), diff --git a/test/ejabberd_SUITE_data/ejabberd.yml b/test/ejabberd_SUITE_data/ejabberd.yml index 2d2e098de..3a6d4947f 100644 --- a/test/ejabberd_SUITE_data/ejabberd.yml +++ b/test/ejabberd_SUITE_data/ejabberd.yml @@ -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 index 000000000..c89b97742 --- /dev/null +++ b/test/muc_tests.erl @@ -0,0 +1,1877 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 15 Oct 2016 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-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 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. diff --git a/test/suite.erl b/test/suite.erl index ed1cbd83d..7a823844b 100644 --- a/test/suite.erl +++ b/test/suite.erl @@ -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) -> {<>, @@ -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.