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]).
   30: 
   31: %%--------------------------------------------------------------------
   32: %% Suite configuration
   33: %%--------------------------------------------------------------------
   34: 
   35: all() ->
   36:     [{group, positive},
   37:      {group, negative}].
   38: 
   39: groups() ->
   40:     G = [{positive, [parallel], success_response() ++ complex()},
   41:          {negative, [parallel], failure_response()}],
   42:     ct_helper:repeat_all_until_all_ok(G).
   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:     [failed_invites,
   60:      failed_messages].
   61: 
   62: %%--------------------------------------------------------------------
   63: %% Init & teardown
   64: %%--------------------------------------------------------------------
   65: 
   66: init_per_suite(Config) ->
   67:     muc_helper:load_muc(),
   68:     escalus:init_per_suite(Config).
   69: 
   70: end_per_suite(Config) ->
   71:     muc_helper:unload_muc(),
   72:     escalus_fresh:clean(),
   73:     escalus:end_per_suite(Config).
   74: 
   75: init_per_group(_GroupName, Config) ->
   76:     escalus:create_users(Config, escalus:get_users([alice, bob, kate])).
   77: 
   78: end_per_group(_GroupName, Config) ->
   79:     escalus:delete_users(Config, escalus:get_users([alice, bob, kate])).
   80: 
   81: init_per_testcase(CaseName, Config0) ->
   82:     Config1 = [{room_name, make_distinct_name(<<"wonderland">>)}|Config0],
   83:     escalus:init_per_testcase(CaseName, Config1).
   84: 
   85: end_per_testcase(CaseName, Config) ->
   86:     muc_helper:destroy_room(muc_helper:muc_host(), ?config(room_name, Config)),
   87:     escalus:end_per_testcase(CaseName, Config).
   88: 
   89: 
   90: %%--------------------------------------------------------------------
   91: %% Tests
   92: %%--------------------------------------------------------------------
   93: 
   94: create_room(Config) ->
   95:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
   96:         Path = path([]),
   97:         Name = ?config(room_name, Config),
   98:         Body = #{name => Name,
   99:                  owner => escalus_client:short_jid(Alice),
  100:                  nick => <<"ali">>},
  101:         Res = rest_helper:make_request(#{role => admin,
  102:                                          method => <<"POST">>,
  103:                                          path => Path,
  104:                                          body => Body,
  105:                                          return_headers => true}),
  106:         {{<<"201">>, _}, Headers, Name} = Res,
  107:         Exp = <<"/api", (path([Name]))/binary>>,
  108:         Uri = uri_string:parse(proplists:get_value(<<"location">>, Headers)),
  109:         ?assertEqual(Exp, maps:get(path, Uri)),
  110:         %% Service acknowledges room creation (10.1.1 Ex. 154), then
  111:         %% (presumably 7.2.16) sends room subject, finally the IQ
  112:         %% result of the IQ request (10.1.2) for an instant room. The
  113:         %% stanza for 7.2.16 has a BODY element which it shouldn't.
  114:         escalus:wait_for_stanzas(Alice, 3),
  115:         escalus:send(Alice, stanza_get_rooms()),
  116:         Stanza = escalus:wait_for_stanza(Alice),
  117:         true = has_room(muc_helper:room_address(Name), Stanza),
  118:         escalus:assert(is_stanza_from, [muc_helper:muc_host()], Stanza)
  119:     end).
  120: 
  121: invite_online_user_to_room(Config) ->
  122:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  123:         Name = ?config(room_name, Config),
  124:         Path = path([Name, "participants"]),
  125:         Reason = <<"I think you'll like this room!">>,
  126:         Body = #{sender => escalus_client:short_jid(Alice),
  127:                  recipient => escalus_client:short_jid(Bob),
  128:                  reason => Reason},
  129:         {{<<"404">>, _}, <<"Room not found">>} = rest_helper:post(admin, Path, Body),
  130:         set_up_room(Config, Alice),
  131:         {{<<"204">>, _}, <<"">>} = rest_helper:post(admin, Path, Body),
  132:         Stanza = escalus:wait_for_stanza(Bob),
  133:         is_direct_invitation(Stanza),
  134:         direct_invite_has_reason(Stanza, Reason)
  135:     end).
  136: 
  137: send_message_to_room(Config) ->
  138:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(_Alice, Bob) ->
  139:         Name = ?config(room_name, Config),
  140:         %% Alice creates a MUC room.
  141:         muc_helper:start_room([], escalus_users:get_user_by_name(alice),
  142:                               Name, <<"ali">>, []),
  143:         %% Bob enters the room.
  144:         escalus:send(Bob,
  145:                      muc_helper:stanza_muc_enter_room(Name,
  146:                                                       <<"bobcat">>)),
  147:         escalus:wait_for_stanzas(Bob, 2),
  148:         %% Parameters for this test.
  149:         Path = path([Name, "messages"]),
  150:         Message = <<"Greetings!">>,
  151:         Body = #{from => escalus_client:short_jid(Bob),
  152:                  body => Message},
  153:         {{<<"204">>, _}, <<"">>} = rest_helper:post(admin, Path, Body),
  154:         Got = escalus:wait_for_stanza(Bob),
  155:         escalus:assert(is_message, Got),
  156:         Message = exml_query:path(Got, [{element, <<"body">>}, cdata])
  157:     end).
  158: 
  159: kick_user_from_room(Config) ->
  160:     escalus:fresh_story(Config,
  161:       [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  162:         %% Parameters for this test.
  163:         Name = ?config(room_name, Config),
  164:         Path = path([Name, "bobcat"]),
  165:         %% Alice creates and enters the room.
  166:         escalus:send(Alice,
  167:                      muc_helper:stanza_muc_enter_room(Name,
  168:                                                       <<"alibaba">>)),
  169:         escalus:send(Alice,
  170:                      muc_helper:stanza_default_muc_room(Name,
  171:                                                         <<"alibaba">>)),
  172:         %% Alice gets an IQ result, her affiliation information, and
  173:         %% the room's subject line.
  174:         escalus:wait_for_stanzas(Alice, 3),
  175:         %% Bob enters the room.
  176:         escalus:send(Bob,
  177:                      muc_helper:stanza_muc_enter_room(Name,
  178:                                                       <<"bobcat">>)),
  179:         escalus:wait_for_stanzas(Bob, 3),
  180:         %% Alice sees Bob's presence.
  181:         escalus:wait_for_stanza(Alice),
  182:         %% Kate enters the room.
  183:         escalus:send(Kate,
  184:                      muc_helper:stanza_muc_enter_room(Name,
  185:                                                       <<"kitkat">>)),
  186:         escalus:wait_for_stanzas(Kate, 4),
  187:         %% Alice and Bob see Kate's presence.
  188:         escalus:wait_for_stanza(Alice),
  189:         escalus:wait_for_stanza(Bob),
  190:         %% The HTTP call in question.
  191:         {{<<"204">>, _}, <<"">>} = rest_helper:delete(admin, Path),
  192:         BobRoomAddress = muc_helper:room_address(Name, <<"bobcat">>),
  193:         %% Bob finds out he's been kicked.
  194:         KickedStanza = escalus:wait_for_stanza(Bob),
  195:         is_unavailable_presence_from(KickedStanza, BobRoomAddress),
  196:         %% Kate finds out Bob is kicked.
  197:         is_unavailable_presence_from(escalus:wait_for_stanza(Kate),
  198:                                      BobRoomAddress),
  199:         %% Alice finds out Bob is kicked.
  200:         is_unavailable_presence_from(escalus:wait_for_stanza(Alice),
  201:                                      BobRoomAddress),
  202:         %% **NOTE**: Alice is a moderator so Bob is kicked through
  203:         %% her. She recieves and IQ result.
  204:         escalus:wait_for_stanza(Alice)
  205:     end).
  206: 
  207: multiparty_multiprotocol(Config) ->
  208:     MUCPath = path([]),
  209:     Room = ?config(room_name, Config),
  210:     RoomInvitePath = path([Room, "participants"]),
  211:     Reason = <<"I think you'll like this room!">>,
  212:     MessagePath = path([Room, "messages"]),
  213:     Message = <<"Greetings!">>,
  214:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  215:         fun(Alice, Bob, Kate) ->
  216:             %% XMPP: Bob does not see a MUC room called 'wonderland'.
  217:             false = user_sees_room(Bob, Room),
  218:             %% HTTP: create a room on Alice's behalf.
  219:             {{<<"201">>, _}, Room} =
  220:                 rest_helper:post(admin, MUCPath,
  221:                                  #{name => Room,
  222:                                    owner => escalus_client:short_jid(Alice),
  223:                                    nick => <<"ali">>}),
  224:             %% See comments under the create room test case.
  225:             escalus:wait_for_stanzas(Alice, 3),
  226:             %% XMPP: Kate sees the MUC room.
  227:             true = user_sees_room(Kate, Room),
  228:             %% HTTP: Alice invites Bob to the MUC room.
  229:             {{<<"204">>, _}, <<"">>} =
  230:                 rest_helper:post(admin, RoomInvitePath,
  231:                                  invite_body(Alice, Bob, Reason)),
  232:             %% XMPP: Bob recieves the invite to the MUC room.
  233:             Room = wait_for_invite(Bob, Reason),
  234:             %% HTTP: Alice invites Kate to the MUC room.
  235:             {{<<"204">>, _}, <<"">>} =
  236:                 rest_helper:post(admin, RoomInvitePath,
  237:                                  invite_body(Alice, Kate, Reason)),
  238:             %% XMPP: kate recieves the invite to the MUC room.
  239:             Room = wait_for_invite(Kate, Reason),
  240:             %% XMPP: Bob joins the MUC room with the JID he recieved.
  241:             escalus:send(Bob,
  242:                          muc_helper:stanza_muc_enter_room(Room,
  243:                                                           <<"bobcat">>)),
  244:             %% Bob gets precense informing him of his room occupancy,
  245:             %% he recieves a presence informing him about Alice's
  246:             %% affiliation and occupancy, and (presumably what is
  247:             %% intended to be) the room subject. See 7.1 in the XEP.
  248:             escalus:wait_for_stanzas(Bob, 3),
  249:             %% Alice sees Bob's presence.
  250:             escalus:wait_for_stanza(Alice),
  251:             %% XMPP: kate joins the MUC room with the JID she recieved.
  252:             escalus:send(Kate,
  253:                          muc_helper:stanza_muc_enter_room(Room,
  254:                                                           <<"kitkat">>)),
  255:             %% Kate gets analogous stanza's to Bob + Bob's presence.
  256:             escalus:wait_for_stanzas(Kate, 4),
  257:             %% Alice & Bob get's Kate's presence.
  258:             [ escalus:wait_for_stanza(User) || User <- [Alice, Bob] ],
  259:             %% HTTP: Alice sends a message to the room.
  260:             {{<<"204">>, _}, <<"">>} =
  261:                 rest_helper:post(admin, MessagePath,
  262:                                  #{from => escalus_client:short_jid(Alice),
  263:                                    body => Message}),
  264:             %% XMPP: All three recieve the message sent to the MUC room.
  265:             [ Message = wait_for_group_message(User) || User <- [Alice, Bob, Kate] ],
  266:             %% XMPP: Bob and Kate send a message to the MUC room.
  267:             [ user_sends_message_to_room(U, M, Room)
  268:               || {U, M} <- [{Bob, <<"I'm Bob.">>}, {Kate, <<"I'm Kate.">>}] ],
  269:             %% XMPP: Alice recieves the messages from Bob and Kate.
  270:             BobRoomJID = muc_helper:room_address(Room, <<"bobcat">>),
  271:             KateRoomJID = muc_helper:room_address(Room, <<"kitkat">>),
  272: 
  273:             ?assertEqual([{BobRoomJID, <<"I'm Bob.">>}, {KateRoomJID, <<"I'm Kate.">>}],
  274:                          user_sees_message_from(Alice, Room, 2))
  275:         end).
  276: 
  277: failed_invites(Config) ->
  278:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  279:         Name = set_up_room(Config, Alice),
  280:         BAlice = escalus_client:short_jid(Alice),
  281:         BBob = escalus_client:short_jid(Bob),
  282:         % non-existing room
  283:         {{<<"404">>, _}, <<"Room not found">>} = send_invite(<<"thisroomdoesnotexist">>, BAlice, BBob),
  284:         % invite with bad jid
  285:         {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_invite(Name, BAlice, <<"@badjid">>),
  286:         {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_invite(Name, <<"@badjid">>, BBob),
  287:         ok
  288:     end).
  289: 
  290: failed_messages(Config) ->
  291:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  292:         Name = set_up_room(Config, Alice),
  293:         % non-existing room
  294:         BAlice = escalus_client:short_jid(Alice),
  295:         BBob = escalus_client:short_jid(Bob),
  296:         {{<<"404">>, _}, <<"Room not found">>} = send_invite(<<"thisroomdoesnotexist">>, BAlice, BBob),
  297:         % invite with bad jid
  298:         {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_invite(Name, BAlice, <<"@badjid">>),
  299:         {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_invite(Name, <<"@badjid">>, BBob),
  300:         ok
  301:     end).
  302: 
  303: 
  304: %%--------------------------------------------------------------------
  305: %% Ancillary (adapted from the MUC suite)
  306: %%--------------------------------------------------------------------
  307: 
  308: set_up_room(Config, Alice) ->
  309:     % create a room first
  310:     Name = ?config(room_name, Config),
  311:     Path = path([]),
  312:     Body = #{name => Name,
  313:              owner => escalus_client:short_jid(Alice),
  314:              nick => <<"ali">>},
  315:     Res = rest_helper:post(admin, Path, Body),
  316:     {{<<"201">>, _}, Name} = Res,
  317:     Name.
  318: 
  319: send_invite(RoomName, BinFrom, BinTo) ->
  320:     Path = path([RoomName, "participants"]),
  321:     Reason = <<"I think you'll like this room!">>,
  322:     Body = #{sender => BinFrom,
  323:              recipient => BinTo,
  324:              reason => Reason},
  325:     rest_helper:post(admin, Path, Body).
  326: 
  327: make_distinct_name(Prefix) ->
  328:     {_, S, US} = os:timestamp(),
  329:     L = lists:flatten([integer_to_list(S rem 100), ".", integer_to_list(US)]),
  330:     Suffix = list_to_binary(L),
  331:     %% The bove is adapted from `escalus_fresh'.
  332:     <<Prefix/binary, $-, Suffix/binary>>.
  333: 
  334: stanza_get_rooms() ->
  335:     escalus_stanza:setattr(escalus_stanza:iq_get(?NS_DISCO_ITEMS, []), <<"to">>,
  336:         muc_helper:muc_host()).
  337: 
  338: has_room(JID, #xmlel{children = [ #xmlel{children = Rooms} ]}) ->
  339:     RoomPred = fun(Item) ->
  340:         exml_query:attr(Item, <<"jid">>) == JID
  341:     end,
  342:     lists:any(RoomPred, Rooms).
  343: 
  344: is_direct_invitation(Stanza) ->
  345:     escalus:assert(is_message, Stanza),
  346:     ?NS_JABBER_X_CONF = exml_query:path(Stanza, [{element, <<"x">>}, {attr, <<"xmlns">>}]).
  347: 
  348: direct_invite_has_reason(Stanza, Reason) ->
  349:     Reason = exml_query:path(Stanza, [{element, <<"x">>}, {attr, <<"reason">>}]).
  350: 
  351: invite_body(Sender, Recipient, Reason) ->
  352:     #{sender => escalus_client:short_jid(Sender),
  353:       recipient => escalus_client:short_jid(Recipient),
  354:       reason => Reason}.
  355: 
  356: wait_for_invite(Recipient, Reason) ->
  357:     Stanza = escalus:wait_for_stanza(Recipient),
  358:     is_direct_invitation(Stanza),
  359:     direct_invite_has_reason(Stanza, Reason),
  360:     get_room_name(get_room_jid(Stanza)).
  361: 
  362: get_room_jid(#xmlel{children = [ Invite ]}) ->
  363:     exml_query:attr(Invite, <<"jid">>).
  364: 
  365: wait_for_group_message(Recipient) ->
  366:     Got = escalus:wait_for_stanza(Recipient),
  367:     escalus:assert(is_message, Got),
  368:     exml_query:path(Got, [{element, <<"body">>}, cdata]).
  369: 
  370: user_sees_room(User, Room) ->
  371:     escalus:send(User, stanza_get_rooms()),
  372:     Stanza = escalus:wait_for_stanza(User),
  373:     escalus:assert(is_stanza_from, [muc_helper:muc_host()], Stanza),
  374:     has_room(muc_helper:room_address(Room), Stanza).
  375: 
  376: get_room_name(JID) ->
  377:     escalus_utils:get_username(JID).
  378: 
  379: user_sends_message_to_room(User, Message, Room) ->
  380:     Chat = escalus_stanza:chat_to(muc_helper:room_address(Room), Message),
  381:     Stanza = escalus_stanza:setattr(Chat, <<"type">>, <<"groupchat">>),
  382:     escalus:send(User, muc_helper:stanza_to_room(Stanza, Room)).
  383: 
  384: user_sees_message_from(User, Room, Times) ->
  385:     user_sees_message_from(User, Room, Times, []).
  386: 
  387: user_sees_message_from(_, _, 0, Messages) ->
  388:     lists:sort(Messages);
  389: user_sees_message_from(User, Room, Times, Messages) ->
  390:     Stanza = escalus:wait_for_stanza(User),
  391:     UserRoomJID = exml_query:path(Stanza, [{attr, <<"from">>}]),
  392:     Body = exml_query:path(Stanza, [{element, <<"body">>}, cdata]),
  393:     user_sees_message_from(User, Room, Times - 1, [{UserRoomJID, Body} | Messages]).
  394: 
  395: is_unavailable_presence_from(Stanza, RoomJID) ->
  396:     escalus:assert(is_presence_with_type, [<<"unavailable">>], Stanza),
  397:     escalus_assert:is_stanza_from(RoomJID, Stanza).
  398: 
  399: path(Items) ->
  400:     AllItems = ["mucs", domain() | Items],
  401:     iolist_to_binary([[$/, Item] || Item <- AllItems]).