1: %%==============================================================================
    2: %% Copyright 2016 Erlang Solutions Ltd.
    3: %%
    4: %% Licensed under the Apache License, Version 2.0 (the "License");
    5: %% you may not use this file except in compliance with the License.
    6: %% You may obtain a copy of the License at
    7: %%
    8: %% http://www.apache.org/licenses/LICENSE-2.0
    9: %%
   10: %% Unless required by applicable law or agreed to in writing, software
   11: %% distributed under the License is distributed on an "AS IS" BASIS,
   12: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   13: %% See the License for the specific language governing permissions and
   14: %% limitations under the License.
   15: %%
   16: %% Author: Joseph Yiasemides <joseph.yiasemides@erlang-solutions.com>
   17: %% Description: Test HTTP Administration API for Mult-user Chat (MUC)
   18: %%==============================================================================
   19: 
   20: -module(muc_http_api_SUITE).
   21: -compile([export_all, nowarn_export_all]).
   22: 
   23: -include_lib("escalus/include/escalus.hrl").
   24: -include_lib("escalus/include/escalus_xmlns.hrl").
   25: -include_lib("common_test/include/ct.hrl").
   26: -include_lib("eunit/include/eunit.hrl").
   27: -include_lib("exml/include/exml.hrl").
   28: 
   29: -import(domain_helper, [domain/0, secondary_domain/0]).
   30: -import(rest_helper, [post/3, delete/2]).
   31: 
   32: %%--------------------------------------------------------------------
   33: %% Suite configuration
   34: %%--------------------------------------------------------------------
   35: 
   36: all() ->
   37:     [{group, positive},
   38:      {group, negative}].
   39: 
   40: groups() ->
   41:     [{positive, [parallel], success_response() ++ complex()},
   42:      {negative, [parallel], failure_response()}].
   43: 
   44: success_response() ->
   45:     [
   46:      create_room,
   47:      invite_online_user_to_room,
   48:      kick_user_from_room,
   49:      %% invite_offline_user_to_room, %% TO DO.
   50:      send_message_to_room
   51:     ].
   52: 
   53: complex() ->
   54:     [
   55:      multiparty_multiprotocol
   56:     ].
   57: 
   58: failure_response() ->
   59:     [room_creation_errors,
   60:      invite_errors,
   61:      kick_user_errors,
   62:      message_errors].
   63: 
   64: %%--------------------------------------------------------------------
   65: %% Init & teardown
   66: %%--------------------------------------------------------------------
   67: 
   68: init_per_suite(Config) ->
   69:     muc_helper:load_muc(),
   70:     escalus:init_per_suite(Config).
   71: 
   72: end_per_suite(Config) ->
   73:     muc_helper:unload_muc(),
   74:     escalus_fresh:clean(),
   75:     escalus:end_per_suite(Config).
   76: 
   77: init_per_group(_GroupName, Config) ->
   78:     escalus:create_users(Config, escalus:get_users([alice, bob, kate])).
   79: 
   80: end_per_group(_GroupName, Config) ->
   81:     escalus:delete_users(Config, escalus:get_users([alice, bob, kate])).
   82: 
   83: init_per_testcase(CaseName, Config0) ->
   84:     Config1 = [{room_name, make_distinct_name(<<"wonderland">>)}|Config0],
   85:     escalus:init_per_testcase(CaseName, Config1).
   86: 
   87: end_per_testcase(CaseName, Config) ->
   88:     muc_helper:destroy_room(muc_helper:muc_host(), ?config(room_name, Config)),
   89:     escalus:end_per_testcase(CaseName, Config).
   90: 
   91: 
   92: %%--------------------------------------------------------------------
   93: %% Tests
   94: %%--------------------------------------------------------------------
   95: 
   96: create_room(Config) ->
   97:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
   98:         Path = path([]),
   99:         Name = ?config(room_name, Config),
  100:         Body = #{name => Name,
  101:                  owner => escalus_client:short_jid(Alice),
  102:                  nick => <<"ali">>},
  103:         Res = rest_helper:make_request(#{role => admin,
  104:                                          method => <<"POST">>,
  105:                                          path => Path,
  106:                                          body => Body,
  107:                                          return_headers => true}),
  108:         {{<<"201">>, _}, Headers, Name} = Res,
  109:         Exp = <<"/api", (path([Name]))/binary>>,
  110:         Uri = uri_string:parse(proplists:get_value(<<"location">>, Headers)),
  111:         ?assertEqual(Exp, maps:get(path, Uri)),
  112:         %% Service acknowledges room creation (10.1.1 Ex. 154), then
  113:         %% (presumably 7.2.16) sends room subject, finally the IQ
  114:         %% result of the IQ request (10.1.2) for an instant room. The
  115:         %% stanza for 7.2.16 has a BODY element which it shouldn't.
  116:         escalus:wait_for_stanzas(Alice, 3),
  117:         escalus:send(Alice, stanza_get_rooms()),
  118:         Stanza = escalus:wait_for_stanza(Alice),
  119:         true = has_room(muc_helper:room_address(Name), Stanza),
  120:         escalus:assert(is_stanza_from, [muc_helper:muc_host()], Stanza)
  121:     end).
  122: 
  123: invite_online_user_to_room(Config) ->
  124:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  125:         Name = ?config(room_name, Config),
  126:         Path = path([Name, "participants"]),
  127:         Reason = <<"I think you'll like this room!">>,
  128:         Body = #{sender => escalus_client:short_jid(Alice),
  129:                  recipient => escalus_client:short_jid(Bob),
  130:                  reason => Reason},
  131:         {{<<"404">>, _}, <<"Room not found">>} = rest_helper:post(admin, Path, Body),
  132:         set_up_room(Config, escalus_client:short_jid(Alice)),
  133:         {{<<"204">>, _}, <<"">>} = rest_helper:post(admin, Path, Body),
  134:         Stanza = escalus:wait_for_stanza(Bob),
  135:         is_direct_invitation(Stanza),
  136:         direct_invite_has_reason(Stanza, Reason)
  137:     end).
  138: 
  139: send_message_to_room(Config) ->
  140:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(_Alice, Bob) ->
  141:         Name = ?config(room_name, Config),
  142:         %% Alice creates a MUC room.
  143:         muc_helper:start_room([], escalus_users:get_user_by_name(alice),
  144:                               Name, <<"ali">>, []),
  145:         %% Bob enters the room.
  146:         escalus:send(Bob,
  147:                      muc_helper:stanza_muc_enter_room(Name,
  148:                                                       <<"bobcat">>)),
  149:         escalus:wait_for_stanzas(Bob, 2),
  150:         %% Parameters for this test.
  151:         Path = path([Name, "messages"]),
  152:         Message = <<"Greetings!">>,
  153:         Body = #{from => escalus_client:short_jid(Bob),
  154:                  body => Message},
  155:         {{<<"204">>, _}, <<"">>} = rest_helper:post(admin, Path, Body),
  156:         Got = escalus:wait_for_stanza(Bob),
  157:         escalus:assert(is_message, Got),
  158:         Message = exml_query:path(Got, [{element, <<"body">>}, cdata])
  159:     end).
  160: 
  161: kick_user_from_room(Config) ->
  162:     escalus:fresh_story(Config,
  163:       [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  164:         %% Parameters for this test.
  165:         Name = ?config(room_name, Config),
  166:         Path = path([Name, "bobcat"]),
  167:         %% Alice creates and enters the room.
  168:         escalus:send(Alice,
  169:                      muc_helper:stanza_muc_enter_room(Name,
  170:                                                       <<"alibaba">>)),
  171:         escalus:send(Alice,
  172:                      muc_helper:stanza_default_muc_room(Name,
  173:                                                         <<"alibaba">>)),
  174:         %% Alice gets an IQ result, her affiliation information, and
  175:         %% the room's subject line.
  176:         escalus:wait_for_stanzas(Alice, 3),
  177:         %% Bob enters the room.
  178:         escalus:send(Bob,
  179:                      muc_helper:stanza_muc_enter_room(Name,
  180:                                                       <<"bobcat">>)),
  181:         escalus:wait_for_stanzas(Bob, 3),
  182:         %% Alice sees Bob's presence.
  183:         escalus:wait_for_stanza(Alice),
  184:         %% Kate enters the room.
  185:         escalus:send(Kate,
  186:                      muc_helper:stanza_muc_enter_room(Name,
  187:                                                       <<"kitkat">>)),
  188:         escalus:wait_for_stanzas(Kate, 4),
  189:         %% Alice and Bob see Kate's presence.
  190:         escalus:wait_for_stanza(Alice),
  191:         escalus:wait_for_stanza(Bob),
  192:         %% The HTTP call in question.
  193:         {{<<"204">>, _}, <<"">>} = rest_helper:delete(admin, Path),
  194:         BobRoomAddress = muc_helper:room_address(Name, <<"bobcat">>),
  195:         %% Bob finds out he's been kicked.
  196:         KickedStanza = escalus:wait_for_stanza(Bob),
  197:         is_unavailable_presence_from(KickedStanza, BobRoomAddress),
  198:         %% Kate finds out Bob is kicked.
  199:         is_unavailable_presence_from(escalus:wait_for_stanza(Kate),
  200:                                      BobRoomAddress),
  201:         %% Alice finds out Bob is kicked.
  202:         is_unavailable_presence_from(escalus:wait_for_stanza(Alice),
  203:                                      BobRoomAddress),
  204:         %% **NOTE**: Alice is a moderator so Bob is kicked through
  205:         %% her. She recieves and IQ result.
  206:         escalus:wait_for_stanza(Alice)
  207:     end).
  208: 
  209: multiparty_multiprotocol(Config) ->
  210:     MUCPath = path([]),
  211:     Room = ?config(room_name, Config),
  212:     RoomInvitePath = path([Room, "participants"]),
  213:     Reason = <<"I think you'll like this room!">>,
  214:     MessagePath = path([Room, "messages"]),
  215:     Message = <<"Greetings!">>,
  216:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  217:         fun(Alice, Bob, Kate) ->
  218:             %% XMPP: Bob does not see a MUC room called 'wonderland'.
  219:             false = user_sees_room(Bob, Room),
  220:             %% HTTP: create a room on Alice's behalf.
  221:             {{<<"201">>, _}, Room} =
  222:                 rest_helper:post(admin, MUCPath,
  223:                                  #{name => Room,
  224:                                    owner => escalus_client:short_jid(Alice),
  225:                                    nick => <<"ali">>}),
  226:             %% See comments under the create room test case.
  227:             escalus:wait_for_stanzas(Alice, 3),
  228:             %% XMPP: Kate sees the MUC room.
  229:             true = user_sees_room(Kate, Room),
  230:             %% HTTP: Alice invites Bob to the MUC room.
  231:             {{<<"204">>, _}, <<"">>} =
  232:                 rest_helper:post(admin, RoomInvitePath,
  233:                                  invite_body(Alice, Bob, Reason)),
  234:             %% XMPP: Bob recieves the invite to the MUC room.
  235:             Room = wait_for_invite(Bob, Reason),
  236:             %% HTTP: Alice invites Kate to the MUC room.
  237:             {{<<"204">>, _}, <<"">>} =
  238:                 rest_helper:post(admin, RoomInvitePath,
  239:                                  invite_body(Alice, Kate, Reason)),
  240:             %% XMPP: kate recieves the invite to the MUC room.
  241:             Room = wait_for_invite(Kate, Reason),
  242:             %% XMPP: Bob joins the MUC room with the JID he recieved.
  243:             escalus:send(Bob,
  244:                          muc_helper:stanza_muc_enter_room(Room,
  245:                                                           <<"bobcat">>)),
  246:             %% Bob gets precense informing him of his room occupancy,
  247:             %% he recieves a presence informing him about Alice's
  248:             %% affiliation and occupancy, and (presumably what is
  249:             %% intended to be) the room subject. See 7.1 in the XEP.
  250:             escalus:wait_for_stanzas(Bob, 3),
  251:             %% Alice sees Bob's presence.
  252:             escalus:wait_for_stanza(Alice),
  253:             %% XMPP: kate joins the MUC room with the JID she recieved.
  254:             escalus:send(Kate,
  255:                          muc_helper:stanza_muc_enter_room(Room,
  256:                                                           <<"kitkat">>)),
  257:             %% Kate gets analogous stanza's to Bob + Bob's presence.
  258:             escalus:wait_for_stanzas(Kate, 4),
  259:             %% Alice & Bob get's Kate's presence.
  260:             [ escalus:wait_for_stanza(User) || User <- [Alice, Bob] ],
  261:             %% HTTP: Alice sends a message to the room.
  262:             {{<<"204">>, _}, <<"">>} =
  263:                 rest_helper:post(admin, MessagePath,
  264:                                  #{from => escalus_client:short_jid(Alice),
  265:                                    body => Message}),
  266:             %% XMPP: All three recieve the message sent to the MUC room.
  267:             [ Message = wait_for_group_message(User) || User <- [Alice, Bob, Kate] ],
  268:             %% XMPP: Bob and Kate send a message to the MUC room.
  269:             [ user_sends_message_to_room(U, M, Room)
  270:               || {U, M} <- [{Bob, <<"I'm Bob.">>}, {Kate, <<"I'm Kate.">>}] ],
  271:             %% XMPP: Alice recieves the messages from Bob and Kate.
  272:             BobRoomJID = muc_helper:room_address(Room, <<"bobcat">>),
  273:             KateRoomJID = muc_helper:room_address(Room, <<"kitkat">>),
  274: 
  275:             ?assertEqual([{BobRoomJID, <<"I'm Bob.">>}, {KateRoomJID, <<"I'm Kate.">>}],
  276:                          user_sees_message_from(Alice, Room, 2))
  277:         end).
  278: 
  279: room_creation_errors(Config) ->
  280:     Config1 = escalus_fresh:create_users(Config, [{alice, 1}]),
  281:     AliceJid = escalus_users:get_jid(Config1, alice),
  282:     Name = ?config(room_name, Config),
  283:     Body = #{name => Name, owner => AliceJid, nick => <<"nick">>},
  284:     {{<<"400">>, _}, <<"Missing room name">>} =
  285:         post(admin, <<"/mucs/", (domain())/binary>>, maps:remove(name, Body)),
  286:     {{<<"400">>, _}, <<"Missing nickname">>} =
  287:         post(admin, <<"/mucs/", (domain())/binary>>, maps:remove(nick, Body)),
  288:     {{<<"400">>, _}, <<"Missing owner JID">>} =
  289:         post(admin, <<"/mucs/", (domain())/binary>>, maps:remove(owner, Body)),
  290:     {{<<"400">>, _}, <<"Invalid room name">>} =
  291:         post(admin, <<"/mucs/", (domain())/binary>>, Body#{name := <<"@invalid">>}),
  292:     {{<<"400">>, _}, <<"Invalid owner JID">>} =
  293:         post(admin, <<"/mucs/", (domain())/binary>>, Body#{owner := <<"@invalid">>}),
  294:     {{<<"404">>, _}, <<"Given user not found">>} =
  295:         post(admin, <<"/mucs/", (domain())/binary>>, Body#{owner := <<"baduser@baddomain">>}).
  296: 
  297: invite_errors(Config) ->
  298:     Config1 = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  299:     AliceJid = escalus_users:get_jid(Config1, alice),
  300:     BobJid = escalus_users:get_jid(Config1, bob),
  301:     Name = set_up_room(Config1, AliceJid),
  302:     Path = path([Name, "participants"]),
  303:     Body = #{sender => AliceJid, recipient => BobJid, reason => <<"Join this room!">>},
  304:     {{<<"400">>, _}, <<"Missing sender JID">>} =
  305:         post(admin, Path, maps:remove(sender, Body)),
  306:     {{<<"400">>, _}, <<"Missing recipient JID">>} =
  307:         post(admin, Path, maps:remove(recipient, Body)),
  308:     {{<<"400">>, _}, <<"Missing invite reason">>} =
  309:         post(admin, Path, maps:remove(reason, Body)),
  310:     {{<<"400">>, _}, <<"Invalid recipient JID">>} =
  311:         post(admin, Path, Body#{recipient := <<"@badjid">>}),
  312:     {{<<"400">>, _}, <<"Invalid sender JID">>} =
  313:         post(admin, Path, Body#{sender := <<"@badjid">>}),
  314:     {{<<"404">>, _}, <<"MUC domain not found">>} =
  315:         post(admin, <<"/mucs/baddomain/", Name/binary, "/participants">>, Body),
  316:     {{<<"404">>, _}, <<"Room not found">>} =
  317:         post(admin, path(["thisroomdoesnotexist", "participants"]), Body).
  318: 
  319: kick_user_errors(Config) ->
  320:     Config1 = escalus_fresh:create_users(Config, [{alice, 1}]),
  321:     AliceJid = escalus_users:get_jid(Config1, alice),
  322:     Name = ?config(room_name, Config1),
  323:     {{<<"404">>, _}, <<"Room not found">>} = delete(admin, path([Name, "nick"])),
  324:     set_up_room(Config1, AliceJid),
  325:     mongoose_helper:wait_until(fun() -> check_if_moderator_not_found(Name) end, ok),
  326:     %% Alice sends presence to the room, making her the moderator
  327:     {ok, Alice} = escalus_client:start(Config1, alice, <<"res1">>),
  328:     escalus:send(Alice, muc_helper:stanza_muc_enter_room(Name, <<"ali">>)),
  329:     %% Alice gets her affiliation information and the room's subject line.
  330:     escalus:wait_for_stanzas(Alice, 2),
  331:     %% Kicking a non-existent nick succeeds in the current implementation
  332:     {{<<"204">>, _}, <<>>} = delete(admin, path([Name, "nick"])),
  333:     escalus_client:stop(Config, Alice).
  334: 
  335: %% @doc Check if the sequence below has already happened:
  336: %%   1. Room notification to the owner is bounced back, because the owner is offline
  337: %%   2. The owner is removed from the online users
  338: %% As a result, a request to kick a user returns Error 404
  339: check_if_moderator_not_found(RoomName) ->
  340:     case delete(admin, path([RoomName, "nick"])) of
  341:         {{<<"404">>, _}, <<"Moderator user not found">>} -> ok;
  342:         {{<<"204">>, _}, _} -> not_yet
  343:     end.
  344: 
  345: message_errors(Config) ->
  346:     Config1 = escalus_fresh:create_users(Config, [{alice, 1}]),
  347:     AliceJid = escalus_users:get_jid(Config1, alice),
  348:     Name = set_up_room(Config1, AliceJid),
  349:     Path = path([Name, "messages"]),
  350:     Body = #{from => AliceJid, body => <<"Greetings!">>},
  351:     % Message to a non-existent room succeeds in the current implementation
  352:     {{<<"204">>, _}, <<>>} = post(admin, path(["thisroomdoesnotexist", "messages"]), Body),
  353:     {{<<"400">>, _}, <<"Missing message body">>} = post(admin, Path, maps:remove(body, Body)),
  354:     {{<<"400">>, _}, <<"Missing sender JID">>} = post(admin, Path, maps:remove(from, Body)),
  355:     {{<<"400">>, _}, <<"Invalid sender JID">>} = post(admin, Path, Body#{from := <<"@invalid">>}).
  356: 
  357: %%--------------------------------------------------------------------
  358: %% Ancillary (adapted from the MUC suite)
  359: %%--------------------------------------------------------------------
  360: 
  361: set_up_room(Config, OwnerJID) ->
  362:     % create a room first
  363:     Name = ?config(room_name, Config),
  364:     Path = path([]),
  365:     Body = #{name => Name,
  366:              owner => OwnerJID,
  367:              nick => <<"ali">>},
  368:     Res = rest_helper:post(admin, Path, Body),
  369:     {{<<"201">>, _}, Name} = Res,
  370:     Name.
  371: 
  372: make_distinct_name(Prefix) ->
  373:     {_, S, US} = os:timestamp(),
  374:     L = lists:flatten([integer_to_list(S rem 100), ".", integer_to_list(US)]),
  375:     Suffix = list_to_binary(L),
  376:     %% The bove is adapted from `escalus_fresh'.
  377:     <<Prefix/binary, $-, Suffix/binary>>.
  378: 
  379: stanza_get_rooms() ->
  380:     escalus_stanza:setattr(escalus_stanza:iq_get(?NS_DISCO_ITEMS, []), <<"to">>,
  381:         muc_helper:muc_host()).
  382: 
  383: has_room(JID, #xmlel{children = [ #xmlel{children = Rooms} ]}) ->
  384:     RoomPred = fun(Item) ->
  385:         exml_query:attr(Item, <<"jid">>) == JID
  386:     end,
  387:     lists:any(RoomPred, Rooms).
  388: 
  389: is_direct_invitation(Stanza) ->
  390:     escalus:assert(is_message, Stanza),
  391:     ?NS_JABBER_X_CONF = exml_query:path(Stanza, [{element, <<"x">>}, {attr, <<"xmlns">>}]).
  392: 
  393: direct_invite_has_reason(Stanza, Reason) ->
  394:     Reason = exml_query:path(Stanza, [{element, <<"x">>}, {attr, <<"reason">>}]).
  395: 
  396: invite_body(Sender, Recipient, Reason) ->
  397:     #{sender => escalus_client:short_jid(Sender),
  398:       recipient => escalus_client:short_jid(Recipient),
  399:       reason => Reason}.
  400: 
  401: wait_for_invite(Recipient, Reason) ->
  402:     Stanza = escalus:wait_for_stanza(Recipient),
  403:     is_direct_invitation(Stanza),
  404:     direct_invite_has_reason(Stanza, Reason),
  405:     get_room_name(get_room_jid(Stanza)).
  406: 
  407: get_room_jid(#xmlel{children = [ Invite ]}) ->
  408:     exml_query:attr(Invite, <<"jid">>).
  409: 
  410: wait_for_group_message(Recipient) ->
  411:     Got = escalus:wait_for_stanza(Recipient),
  412:     escalus:assert(is_message, Got),
  413:     exml_query:path(Got, [{element, <<"body">>}, cdata]).
  414: 
  415: user_sees_room(User, Room) ->
  416:     escalus:send(User, stanza_get_rooms()),
  417:     Stanza = escalus:wait_for_stanza(User),
  418:     escalus:assert(is_stanza_from, [muc_helper:muc_host()], Stanza),
  419:     has_room(muc_helper:room_address(Room), Stanza).
  420: 
  421: get_room_name(JID) ->
  422:     escalus_utils:get_username(JID).
  423: 
  424: user_sends_message_to_room(User, Message, Room) ->
  425:     Chat = escalus_stanza:chat_to(muc_helper:room_address(Room), Message),
  426:     Stanza = escalus_stanza:setattr(Chat, <<"type">>, <<"groupchat">>),
  427:     escalus:send(User, muc_helper:stanza_to_room(Stanza, Room)).
  428: 
  429: user_sees_message_from(User, Room, Times) ->
  430:     user_sees_message_from(User, Room, Times, []).
  431: 
  432: user_sees_message_from(_, _, 0, Messages) ->
  433:     lists:sort(Messages);
  434: user_sees_message_from(User, Room, Times, Messages) ->
  435:     Stanza = escalus:wait_for_stanza(User),
  436:     UserRoomJID = exml_query:path(Stanza, [{attr, <<"from">>}]),
  437:     Body = exml_query:path(Stanza, [{element, <<"body">>}, cdata]),
  438:     user_sees_message_from(User, Room, Times - 1, [{UserRoomJID, Body} | Messages]).
  439: 
  440: is_unavailable_presence_from(Stanza, RoomJID) ->
  441:     escalus:assert(is_presence_with_type, [<<"unavailable">>], Stanza),
  442:     escalus_assert:is_stanza_from(RoomJID, Stanza).
  443: 
  444: path(Items) ->
  445:     AllItems = ["mucs", domain() | Items],
  446:     iolist_to_binary([[$/, Item] || Item <- AllItems]).