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