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 MUC Light
   18: %%==============================================================================
   19: 
   20: -module(muc_light_http_api_SUITE).
   21: -compile([export_all, nowarn_export_all]).
   22: 
   23: -include_lib("common_test/include/ct.hrl").
   24: -include_lib("eunit/include/eunit.hrl").
   25: -include_lib("escalus/include/escalus.hrl").
   26: -include_lib("escalus/include/escalus_xmlns.hrl").
   27: -include_lib("exml/include/exml.hrl").
   28: 
   29: -import(muc_light_helper, [stanza_create_room/3]).
   30: -import(distributed_helper, [subhost_pattern/1]).
   31: -import(domain_helper, [host_type/0, domain/0]).
   32: 
   33: %%--------------------------------------------------------------------
   34: %% Suite configuration
   35: %%--------------------------------------------------------------------
   36: 
   37: all() ->
   38:     [{group, positive},
   39:      {group, negative}].
   40: 
   41: groups() ->
   42:     [{positive, [parallel], success_response()},
   43:      {negative, [parallel], negative_response()}].
   44: 
   45: success_response() ->
   46:     [create_unique_room,
   47:      create_identifiable_room,
   48:      invite_to_room,
   49:      send_message_to_room,
   50:      delete_room_by_owner
   51:     ].
   52: 
   53: negative_response() ->
   54:     [delete_room_by_non_owner,
   55:      delete_non_existent_room,
   56:      delete_room_without_having_a_membership,
   57:      create_non_unique_room,
   58:      create_room_on_non_existing_muc_server
   59:     ].
   60: 
   61: %%--------------------------------------------------------------------
   62: %% Init & teardown
   63: %%--------------------------------------------------------------------
   64: 
   65: init_per_suite(Config) ->
   66:     dynamic_modules:start(host_type(), mod_muc_light,
   67:         [{host, subhost_pattern(muc_light_helper:muc_host_pattern())},
   68:          {rooms_in_rosters, true},
   69:          {backend, mongoose_helper:mnesia_or_rdbms_backend()}]),
   70:     escalus:init_per_suite(Config).
   71: 
   72: end_per_suite(Config) ->
   73:     escalus_fresh:clean(),
   74:     dynamic_modules:stop(host_type(), mod_muc_light),
   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, Config) ->
   84:     escalus:init_per_testcase(CaseName, Config).
   85: 
   86: end_per_testcase(CaseName, Config) ->
   87:     escalus:end_per_testcase(CaseName, Config).
   88: 
   89: 
   90: %%--------------------------------------------------------------------
   91: %% Tests
   92: %%--------------------------------------------------------------------
   93: 
   94: create_unique_room(Config) ->
   95:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
   96:         MUCLightDomain = muc_light_domain(),
   97:         Path = path([MUCLightDomain]),
   98:         Name = <<"wonderland">>,
   99:         Body = #{ name => Name,
  100:                   owner => escalus_client:short_jid(Alice),
  101:                   subject => <<"Lewis Carol">>
  102:                 },
  103:         {{<<"201">>, _}, _} = rest_helper:post(admin, Path, Body),
  104:         [Item] = get_disco_rooms(Alice),
  105:         true = is_room_name(Name, Item),
  106:         true = is_room_domain(MUCLightDomain, Item)
  107:     end).
  108: 
  109: create_identifiable_room(Config) ->
  110:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  111:         MUCLightDomain = muc_light_domain(),
  112:         Path = path([MUCLightDomain]),
  113:         RandBits = base16:encode(crypto:strong_rand_bytes(5)),
  114:         Name = <<"wonderland">>,
  115:         RoomID = <<"just_some_id_", RandBits/binary>>,
  116:         RoomIDescaped = escalus_utils:jid_to_lower(RoomID),
  117:         Body = #{ id => RoomID,
  118:                   name => Name,
  119:                   owner => escalus_client:short_jid(Alice),
  120:                   subject => <<"Lewis Carol">>
  121:                 },
  122:         {{<<"201">>, _}, RoomJID} = rest_helper:putt(admin, Path, Body),
  123:         [Item] = get_disco_rooms(Alice),
  124:         [RoomIDescaped, MUCLightDomain] = binary:split(RoomJID, <<"@">>),
  125:         true = is_room_name(Name, Item),
  126:         true = is_room_domain(MUCLightDomain, Item),
  127:         true = is_room_id(RoomIDescaped, Item)
  128:     end).
  129: 
  130: invite_to_room(Config) ->
  131:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  132:       fun(Alice, Bob, Kate) ->
  133:         RoomID = atom_to_binary(?FUNCTION_NAME),
  134:         Path = path([muc_light_domain(), RoomID, "participants"]),
  135:         %% XMPP: Alice creates a room.
  136:         Stt = stanza_create_room(RoomID,
  137:             [{<<"roomname">>, <<"wonderland">>}], [{Kate, member}]),
  138:         escalus:send(Alice, Stt),
  139:         %% XMPP: Alice recieves a affiliation message to herself and
  140:         %% an IQ result when creating the MUC Light room.
  141:         escalus:wait_for_stanza(Alice),
  142:         escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  143:         %% (*) HTTP: Invite Bob (change room affiliation) on Alice's behalf.
  144:         Body = #{ sender => escalus_client:short_jid(Alice),
  145:                   recipient => escalus_client:short_jid(Bob)
  146:                 },
  147:         {{<<"204">>, _}, <<"">>} = rest_helper:post(admin, Path, Body),
  148:         %% XMPP: Bob recieves his affiliation information.
  149:         member_is_affiliated(escalus:wait_for_stanza(Bob), Bob),
  150:         %% XMPP: Alice recieves Bob's affiliation infromation.
  151:         member_is_affiliated(escalus:wait_for_stanza(Alice), Bob),
  152:         %% XMPP: Alice does NOT recieve an IQ result stanza following
  153:         %% her HTTP request to invite Bob in story point (*).
  154:         escalus_assert:has_no_stanzas(Alice)
  155:       end).
  156: 
  157: send_message_to_room(Config) ->
  158:     RoomID = atom_to_binary(?FUNCTION_NAME),
  159:     Path = path([muc_light_domain(), RoomID, "messages"]),
  160:     Text = <<"Hello everyone!">>,
  161:     escalus:fresh_story(Config,
  162:       [{alice, 1}, {bob, 1}, {kate, 1}],
  163:       fun(Alice, Bob, Kate) ->
  164:         %% XMPP: Alice creates a room.
  165:         escalus:send(Alice, stanza_create_room(RoomID,
  166:             [{<<"roomname">>, <<"wonderland">>}], [{Bob, member}, {Kate, member}])),
  167:         %% XMPP: Alice gets her own affiliation info
  168:         escalus:wait_for_stanza(Alice),
  169:         %% XMPP: And Alice gets IQ result
  170:         CreationResult = escalus:wait_for_stanza(Alice),
  171:         escalus:assert(is_iq_result, CreationResult),
  172:         %% XMPP: Get Bob and Kate recieve their affiliation information.
  173:         [ escalus:wait_for_stanza(U) || U <- [Bob, Kate] ],
  174:         %% HTTP: Alice sends a message to the MUC room.
  175:         Body = #{ from => escalus_client:short_jid(Alice),
  176:                   body => Text
  177:                 },
  178:         {{<<"204">>, _}, <<"">>} = rest_helper:post(admin, Path, Body),
  179:         %% XMPP: Both Bob and Kate see the message.
  180:         [ see_message_from_user(U, Alice, Text) || U <- [Bob, Kate] ]
  181:     end).
  182: 
  183: delete_room_by_owner(Config) ->
  184:     RoomID = atom_to_binary(?FUNCTION_NAME),
  185:     RoomName = <<"wonderland">>,
  186:     escalus:fresh_story(Config,
  187:                         [{alice, 1}, {bob, 1}, {kate, 1}],
  188:                         fun(Alice, Bob, Kate)->
  189:                                 {{<<"204">>, <<"No Content">>}, <<"">>} =
  190:                                     check_delete_room(Config, RoomName, RoomID, RoomID,
  191:                                                       Alice, [Bob, Kate], Alice)
  192:                         end).
  193: 
  194: delete_room_by_non_owner(Config) ->
  195:     RoomID = atom_to_binary(?FUNCTION_NAME),
  196:     RoomName = <<"wonderland">>,
  197:     escalus:fresh_story(Config,
  198:                         [{alice, 1}, {bob, 1}, {kate, 1}],
  199:                         fun(Alice, Bob, Kate)->
  200:                                 {{<<"403">>, <<"Forbidden">>},
  201:                                  <<"Given user cannot delete this room">>} =
  202:                                     check_delete_room(Config, RoomName, RoomID, RoomID,
  203:                                                       Alice, [Bob, Kate], Bob)
  204:                         end).
  205: 
  206: delete_non_existent_room(Config) ->
  207:     RoomID = atom_to_binary(?FUNCTION_NAME),
  208:     RoomName = <<"wonderland">>,
  209:     escalus:fresh_story(Config,
  210:                         [{alice, 1}, {bob, 1}, {kate, 1}],
  211:                         fun(Alice, Bob, Kate)->
  212:                                 {{<<"404">>, _}, <<"Cannot remove not existing room">>} =
  213:                                     check_delete_room(Config, RoomName, RoomID,
  214:                                                       <<"some_non_existent_room">>,
  215:                                                       Alice, [Bob, Kate], Alice)
  216:                         end).
  217: 
  218: delete_room_without_having_a_membership(Config) ->
  219:     RoomID = atom_to_binary(?FUNCTION_NAME),
  220:     RoomName = <<"wonderland">>,
  221:     escalus:fresh_story(Config,
  222:                         [{alice, 1}, {bob, 1}, {kate, 1}],
  223:                         fun(Alice, Bob, Kate)->
  224:                                 {{<<"403">>, _}, <<"Given user does not occupy this room">>} =
  225:                                     check_delete_room(Config, RoomName, RoomID, RoomID,
  226:                                                       Alice, [Bob], Kate)
  227:                         end).
  228: 
  229: 
  230: create_non_unique_room(Config) ->
  231:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  232:         Path = path([muc_light_domain()]),
  233:         RandBits = base16:encode(crypto:strong_rand_bytes(5)),
  234:         Name = <<"wonderland">>,
  235:         RoomID = <<"just_some_id_", RandBits/binary>>,
  236:         Body = #{ id => RoomID,
  237:                   name => Name,
  238:                   owner => escalus_client:short_jid(Alice),
  239:                   subject => <<"Lewis Carol">>
  240:         },
  241:         {{<<"201">>, _}, _RoomJID} = rest_helper:putt(admin, Path, Body),
  242:         {{<<"403">>, _}, <<"Room already exists">>} = rest_helper:putt(admin, Path, Body),
  243:         ok
  244:     end).
  245: 
  246: create_room_on_non_existing_muc_server(Config) ->
  247:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  248:         Path = path([domain_helper:domain()]),
  249:         Name = <<"wonderland">>,
  250:         Body = #{ name => Name,
  251:                   owner => escalus_client:short_jid(Alice),
  252:                   subject => <<"Lewis Carol">>
  253:                 },
  254:         {{<<"404">>,<<"Not Found">>}, _} = rest_helper:post(admin, Path, Body)
  255:     end).
  256: 
  257: %%--------------------------------------------------------------------
  258: %% Ancillary (borrowed and adapted from the MUC and MUC Light suites)
  259: %%--------------------------------------------------------------------
  260: 
  261: get_disco_rooms(User) ->
  262:     DiscoStanza = escalus_stanza:to(escalus_stanza:iq_get(?NS_DISCO_ITEMS, []), muc_light_domain()),
  263:     escalus:send(User, DiscoStanza),
  264:     Stanza =  escalus:wait_for_stanza(User),
  265:     XNamespaces = exml_query:paths(Stanza, [{element, <<"query">>}, {attr, <<"xmlns">>}]),
  266:     true = lists:member(?NS_DISCO_ITEMS, XNamespaces),
  267:     escalus:assert(is_stanza_from, [muc_light_domain()], Stanza),
  268:     exml_query:paths(Stanza, [{element, <<"query">>}, {element, <<"item">>}]).
  269: 
  270: is_room_name(Name, Item) ->
  271:     Name == exml_query:attr(Item, <<"name">>).
  272: 
  273: is_room_domain(Domain, Item) ->
  274:     JID = exml_query:attr(Item, <<"jid">>),
  275:     [_, Got] = binary:split(JID, <<$@>>, [global]),
  276:     Domain == Got.
  277: 
  278: is_room_id(Id, Item) ->
  279:     JID = exml_query:attr(Item, <<"jid">>),
  280:     [Got, _] = binary:split(JID, <<$@>>, [global]),
  281:     Id == Got.
  282: 
  283: see_message_from_user(User, Sender, Contents) ->
  284:     Stanza = escalus:wait_for_stanza(User),
  285:     #xmlel{ name = <<"message">> } = Stanza,
  286:     SenderJID = escalus_utils:jid_to_lower(escalus_utils:get_short_jid(Sender)),
  287:     From = exml_query:path(Stanza, [{attr, <<"from">>}]),
  288:     {_, _} = binary:match(From, SenderJID),
  289:     Contents = exml_query:path(Stanza, [{element, <<"body">>}, cdata]).
  290: 
  291: member_is_affiliated(Stanza, User) ->
  292:     MemberJID = escalus_utils:jid_to_lower(escalus_utils:get_short_jid(User)),
  293:     Data = exml_query:path(Stanza, [{element, <<"x">>}, {element, <<"user">>}, cdata]),
  294:     MemberJID == Data.
  295: 
  296: check_delete_room(_Config, RoomName, RoomIDToCreate, RoomIDToDelete, RoomOwner,
  297:                   RoomMembers, UserToExecuteDelete) ->
  298:     Members = [{Member, member} || Member <- RoomMembers],
  299:     escalus:send(RoomOwner, stanza_create_room(RoomIDToCreate,
  300:                                            [{<<"roomname">>, RoomName}],
  301:                                            Members)),
  302:     %% XMPP RoomOwner gets affiliation and IQ result
  303:     Affiliations = [{RoomOwner, owner} | Members],
  304:     muc_light_helper:verify_aff_bcast([{RoomOwner, owner}], Affiliations),
  305:     %% and now RoomOwner gets IQ result
  306:     CreationResult = escalus:wait_for_stanza(RoomOwner),
  307:     escalus:assert(is_iq_result, CreationResult),
  308:     muc_light_helper:verify_aff_bcast(Members, Affiliations),
  309:     ShortJID = escalus_client:short_jid(UserToExecuteDelete),
  310:     Path = path([muc_light_domain(), RoomIDToDelete, ShortJID, "management"]),
  311:     rest_helper:delete(admin, Path).
  312: 
  313: 
  314: %%--------------------------------------------------------------------
  315: %% Helpers
  316: %%--------------------------------------------------------------------
  317: 
  318: path(Items) ->
  319:     AllItems = ["muc-lights" | Items],
  320:     iolist_to_binary([[$/, Item] || Item <- AllItems]).
  321: 
  322: muc_light_domain() ->
  323:     muc_light_helper:muc_host().