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]).