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