1: %%============================================================================== 2: %% Copyright 2012-2020 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: 17: -module(rest_SUITE). 18: -compile([export_all, nowarn_export_all]). 19: 20: -include_lib("escalus/include/escalus.hrl"). 21: -include_lib("common_test/include/ct.hrl"). 22: -include_lib("eunit/include/eunit.hrl"). 23: -include_lib("exml/include/exml.hrl"). 24: 25: -import(rest_helper, 26: [assert_inlist/2, 27: assert_notinlist/2, 28: decode_maplist/1, 29: gett/2, 30: gett/3, 31: post/3, 32: putt/3, 33: delete/2] 34: ). 35: -import(domain_helper, [host_type/0, domain/0]). 36: 37: -define(PRT(X, Y), ct:log("~p: ~p", [X, Y])). 38: -define(OK, {<<"200">>, <<"OK">>}). 39: -define(CREATED, {<<"201">>, <<"Created">>}). 40: -define(NOCONTENT, {<<"204">>, <<"No Content">>}). 41: -define(ERROR, {<<"500">>, _}). 42: -define(NOT_FOUND, {<<"404">>, _}). 43: -define(NOT_AUTHORIZED, {<<"401">>, _}). 44: -define(FORBIDDEN, {<<"403">>, _}). 45: -define(BAD_REQUEST, {<<"400">>, _}). 46: 47: %%-------------------------------------------------------------------- 48: %% Suite configuration 49: %%-------------------------------------------------------------------- 50: 51: -define(REGISTRATION_TIMEOUT, 2). %% seconds 52: -define(ATOMS, [name, desc, category, action, security_policy, args, result, sender]). 53: 54: all() -> 55: [ 56: {group, admin}, 57: {group, dynamic_module}, 58: {group, auth}, 59: {group, blank_auth}, 60: {group, roster} 61: ]. 62: 63: groups() -> 64: [{admin, [parallel], test_cases()}, 65: {auth, [parallel], auth_test_cases()}, 66: {blank_auth, [parallel], blank_auth_testcases()}, 67: {roster, [parallel], [list_contacts, 68: befriend_and_alienate, 69: befriend_and_alienate_auto, 70: invalid_roster_operations]}, 71: {dynamic_module, [], [stop_start_command_module]}]. 72: 73: auth_test_cases() -> 74: [auth_passes_correct_creds, 75: auth_fails_incorrect_creds]. 76: 77: blank_auth_testcases() -> 78: [auth_always_passes_blank_creds]. 79: 80: test_cases() -> 81: [commands_are_listed, 82: non_existent_command_returns404, 83: existent_command_with_missing_arguments_returns404, 84: user_can_be_registered_and_removed, 85: sessions_are_listed, 86: session_can_be_kicked, 87: messages_are_sent_and_received, 88: messages_error_handling, 89: stanzas_are_sent_and_received, 90: messages_are_archived, 91: messages_can_be_paginated, 92: password_can_be_changed, 93: types_are_checked_separately_for_args_and_return 94: ]. 95: 96: suite() -> 97: escalus:suite(). 98: 99: %%-------------------------------------------------------------------- 100: %% Init & teardown 101: %%-------------------------------------------------------------------- 102: 103: init_per_suite(Config) -> 104: Config1 = rest_helper:maybe_enable_mam(mam_helper:backend(), host_type(), Config), 105: Config2 = ejabberd_node_utils:init(Config1), 106: escalus:init_per_suite(Config2). 107: 108: end_per_suite(Config) -> 109: escalus_fresh:clean(), 110: rest_helper:maybe_disable_mam(mam_helper:backend(), host_type()), 111: escalus:end_per_suite(Config). 112: 113: init_per_group(auth, Config) -> 114: rest_helper:change_admin_creds({<<"ala">>, <<"makota">>}), 115: Config; 116: init_per_group(blank_auth, Config) -> 117: rest_helper:change_admin_creds(any), 118: Config; 119: init_per_group(_GroupName, Config) -> 120: escalus:create_users(Config, escalus:get_users([alice, bob])). 121: 122: end_per_group(auth, _Config) -> 123: rest_helper:change_admin_creds(any); 124: end_per_group(_GroupName, Config) -> 125: escalus:delete_users(Config, escalus:get_users([alice, bob, mike])). 126: 127: init_per_testcase(types_are_checked_separately_for_args_and_return = CaseName, Config) -> 128: {Mod, Code} = rpc(dynamic_compile, from_string, [custom_module_code()]), 129: rpc(code, load_binary, [Mod, "mod_commands_test.erl", Code]), 130: Config1 = dynamic_modules:save_modules(host_type(), Config), 131: dynamic_modules:ensure_modules(host_type(), [{mod_commands_test, []}]), 132: escalus:init_per_testcase(CaseName, Config1); 133: init_per_testcase(CaseName, Config) -> 134: MAMTestCases = [messages_are_archived, messages_can_be_paginated], 135: rest_helper:maybe_skip_mam_test_cases(CaseName, MAMTestCases, Config). 136: 137: end_per_testcase(types_are_checked_separately_for_args_and_return = CaseName, Config) -> 138: dynamic_modules:restore_modules(Config), 139: escalus:end_per_testcase(CaseName, Config); 140: end_per_testcase(CaseName, Config) -> 141: escalus:end_per_testcase(CaseName, Config). 142: 143: rpc(M, F, A) -> 144: distributed_helper:rpc(distributed_helper:mim(), M, F, A). 145: 146: custom_module_code() -> 147: "-module(mod_commands_test). 148: -export([start/0, stop/0, start/2, stop/1, test_arg/1, test_return/1, supported_features/0]). 149: start() -> mongoose_commands:register(commands()). 150: stop() -> mongoose_commands:unregister(commands()). 151: start(_,_) -> start(). 152: stop(_) -> stop(). 153: supported_features() -> [dynamic_domains]. 154: commands() -> 155: [ 156: [ 157: {name, test_arg}, 158: {category, <<\"test_arg\">>}, 159: {desc, <<\"List test_arg\">>}, 160: {module, mod_commands_test}, 161: {function, test_arg}, 162: {action, create}, 163: {args, [{arg, boolean}]}, 164: {result, [{msg, binary}]} 165: ], 166: [ 167: {name, test_return}, 168: {category, <<\"test_return\">>}, 169: {desc, <<\"List test_return\">>}, 170: {module, mod_commands_test}, 171: {function, test_return}, 172: {action, create}, 173: {args, [{arg, boolean}]}, 174: {result, {msg, binary}} 175: ] 176: ]. 177: test_arg(_) -> <<\"bleble\">>. 178: test_return(_) -> ok. 179: " 180: . 181: 182: %%-------------------------------------------------------------------- 183: %% Tests 184: %%-------------------------------------------------------------------- 185: 186: % Authorization 187: auth_passes_correct_creds(_Config) -> 188: % try to login with the same creds 189: {?OK, _Lcmds} = gett(admin, <<"/commands">>, {<<"ala">>, <<"makota">>}). 190: 191: auth_fails_incorrect_creds(_Config) -> 192: % try to login with different creds 193: {?NOT_AUTHORIZED, _} = gett(admin, <<"/commands">>, {<<"ola">>, <<"mapsa">>}). 194: 195: auth_always_passes_blank_creds(_Config) -> 196: % we set control creds for blank 197: rest_helper:change_admin_creds(any), 198: % try with any auth 199: {?OK, Lcmds} = gett(admin, <<"/commands">>, {<<"aaaa">>, <<"bbbb">>}), 200: % try with no auth 201: {?OK, Lcmds} = gett(admin, <<"/commands">>). 202: 203: commands_are_listed(_C) -> 204: {?OK, Lcmds} = gett(admin, <<"/commands">>), 205: DecCmds = decode_maplist(Lcmds), 206: ListCmd = #{action => <<"read">>, method => <<"GET">>, args => #{}, 207: category => <<"commands">>, 208: desc => <<"List commands">>, 209: name => <<"list_methods">>, 210: path => <<"/commands">>}, 211: %% Check that path and args are listed using a command with args 212: RosterCmd = #{action => <<"read">>, method => <<"GET">>, 213: args => #{caller => <<"string">>}, 214: category => <<"contacts">>, 215: desc => <<"Get roster">>, 216: name => <<"list_contacts">>, 217: path => <<"/contacts/:caller">>}, 218: ?assertEqual([ListCmd], assert_inlist(#{name => <<"list_methods">>}, DecCmds)), 219: ?assertEqual([RosterCmd], assert_inlist(#{name => <<"list_contacts">>}, DecCmds)). 220: 221: non_existent_command_returns404(_C) -> 222: {?NOT_FOUND, _} = gett(admin, <<"/isitthereornot">>). 223: 224: existent_command_with_missing_arguments_returns404(_C) -> 225: {?NOT_FOUND, _} = gett(admin, <<"/contacts/">>). 226: 227: user_can_be_registered_and_removed(_Config) -> 228: % list users 229: {?OK, Lusers} = gett(admin, path("users")), 230: Domain = domain(), 231: assert_inlist(<<"alice@", Domain/binary>>, Lusers), 232: % create user 233: CrUser = #{username => <<"mike">>, password => <<"nicniema">>}, 234: {?CREATED, _} = post(admin, path("users"), CrUser), 235: {?OK, Lusers1} = gett(admin, path("users")), 236: assert_inlist(<<"mike@", Domain/binary>>, Lusers1), 237: % try to create the same user 238: {?FORBIDDEN, _} = post(admin, path("users"), CrUser), 239: % delete user 240: {?NOCONTENT, _} = delete(admin, path("users", ["mike"])), 241: {?OK, Lusers2} = gett(admin, path("users")), 242: assert_notinlist(<<"mike@", Domain/binary>>, Lusers2), 243: % invalid jid 244: CrBadUser = #{username => <<"m@ke">>, password => <<"nicniema">>}, 245: {?BAD_REQUEST, <<"Invalid JID", _/binary>>} = post(admin, path("users"), CrBadUser), 246: {?BAD_REQUEST, <<"Invalid JID", _/binary>>} = delete(admin, path("users", ["@mike"])), 247: %% {?FORBIDDEN, _} = delete(admin, path("users", ["mike"])), % he's already gone, but we 248: %% can't test it because ejabberd_auth_internal:remove_user/2 always returns ok, grrrr 249: ok. 250: 251: sessions_are_listed(_) -> 252: % no session 253: {?OK, Sessions} = gett(admin, path("sessions")), 254: true = is_list(Sessions). 255: 256: session_can_be_kicked(Config) -> 257: escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> 258: % Alice is connected 259: AliceJid = jid:nameprep(escalus_client:full_jid(Alice)), 260: AliceSessionPath = <<"/sessions/", (escalus_client:server(Alice))/binary, 261: "/", (escalus_client:username(Alice))/binary, 262: "/", (escalus_client:resource(Alice))/binary>>, 263: {?OK, Sessions1} = gett(admin, path("sessions")), 264: assert_inlist(AliceJid, Sessions1), 265: % kick alice 266: {?NOCONTENT, _} = delete(admin, AliceSessionPath), 267: escalus:wait_for_stanza(Alice), 268: true = escalus_connection:wait_for_close(Alice, timer:seconds(1)), 269: {?OK, Sessions2} = gett(admin, path("sessions")), 270: assert_notinlist(AliceJid, Sessions2), 271: {?NOT_FOUND, <<"No active session">>} = delete(admin, AliceSessionPath), 272: ok 273: end). 274: 275: messages_are_sent_and_received(Config) -> 276: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 277: {M1, M2} = send_messages(Alice, Bob), 278: Res = escalus:wait_for_stanza(Alice), 279: escalus:assert(is_chat_message, [maps:get(body, M1)], Res), 280: Res1 = escalus:wait_for_stanza(Bob), 281: escalus:assert(is_chat_message, [maps:get(body, M2)], Res1) 282: end). 283: 284: messages_error_handling(Config) -> 285: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 286: AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)), 287: BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), 288: {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_message_bin(AliceJID, <<"@noway">>), 289: {{<<"400">>, _}, <<"Invalid jid:", _/binary>>} = send_message_bin(<<"@noway">>, BobJID), 290: ok 291: end). 292: 293: stanzas_are_sent_and_received(Config) -> 294: %% this is to test the API for sending arbitrary stanzas, e.g. message with extra elements 295: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 296: send_extended_message(Alice, Bob), 297: Res = escalus:wait_for_stanza(Bob), 298: ?assertEqual(<<"attribute">>, exml_query:attr(Res, <<"extra">>)), 299: ?assertEqual(<<"inside the sibling">>, exml_query:path(Res, [{element, <<"sibling">>}, cdata])), 300: Res1 = send_flawed_stanza(missing_attribute, Alice, Bob), 301: {?BAD_REQUEST, <<"both from and to are required">>} = Res1, 302: Res2 = send_flawed_stanza(malformed_xml, Alice, Bob), 303: {?BAD_REQUEST, <<"Malformed stanza: \"expected >\"">>} = Res2, 304: ok 305: end). 306: 307: messages_are_archived(Config) -> 308: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 309: {M1, _M2} = send_messages(Alice, Bob), 310: AliceJID = maps:get(to, M1), 311: BobJID = maps:get(caller, M1), 312: GetPath = lists:flatten(["/messages", 313: "/", binary_to_list(AliceJID), 314: "/", binary_to_list(BobJID), 315: "?limit=10"]), 316: mam_helper:maybe_wait_for_archive(Config), 317: {?OK, Msgs} = gett(admin, GetPath), 318: [Last, Previous|_] = lists:reverse(decode_maplist(Msgs)), 319: <<"hello from Alice">> = maps:get(body, Last), 320: AliceJID = maps:get(sender, Last), 321: <<"hello from Bob">> = maps:get(body, Previous), 322: BobJID = maps:get(sender, Previous), 323: % now if we leave limit out we should get the same result 324: GetPath1 = lists:flatten(["/messages", 325: "/", binary_to_list(AliceJID), 326: "/", binary_to_list(BobJID)]), 327: mam_helper:maybe_wait_for_archive(Config), 328: {?OK, Msgs1} = gett(admin, GetPath1), 329: [Last1, Previous1|_] = lists:reverse(decode_maplist(Msgs1)), 330: <<"hello from Alice">> = maps:get(body, Last1), 331: AliceJID = maps:get(sender, Last1), 332: <<"hello from Bob">> = maps:get(body, Previous1), 333: BobJID = maps:get(sender, Previous1), 334: % and we can do the same without specifying contact 335: GetPath2 = lists:flatten(["/messages/", binary_to_list(AliceJID)]), 336: mam_helper:maybe_wait_for_archive(Config), 337: {?OK, Msgs2} = gett(admin, GetPath2), 338: [Last2, Previous2|_] = lists:reverse(decode_maplist(Msgs2)), 339: <<"hello from Alice">> = maps:get(body, Last2), 340: AliceJID = maps:get(sender, Last2), 341: <<"hello from Bob">> = maps:get(body, Previous2), 342: BobJID = maps:get(sender, Previous2) 343: end). 344: 345: messages_can_be_paginated(Config) -> 346: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 347: AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)), 348: BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), 349: rest_helper:fill_archive(Alice, Bob), 350: mam_helper:maybe_wait_for_archive(Config), 351: % recent msgs with a limit 352: M1 = get_messages(AliceJID, BobJID, 10), 353: ?assertEqual(6, length(M1)), 354: M2 = get_messages(AliceJID, BobJID, 3), 355: ?assertEqual(3, length(M2)), 356: % older messages - earlier then the previous midnight 357: PriorTo = rest_helper:make_timestamp(-1, {0, 0, 1}) div 1000, 358: M3 = get_messages(AliceJID, BobJID, PriorTo, 10), 359: ?assertEqual(4, length(M3)), 360: [Oldest|_] = decode_maplist(M3), 361: ?assertEqual(maps:get(body, Oldest), <<"A">>), 362: % same with limit 363: M4 = get_messages(AliceJID, BobJID, PriorTo, 2), 364: ?assertEqual(2, length(M4)), 365: [Oldest2|_] = decode_maplist(M4), 366: ?assertEqual(maps:get(body, Oldest2), <<"B">>), 367: ok 368: end). 369: 370: password_can_be_changed(Config) -> 371: % bob logs in with his regular password 372: escalus:story(Config, [{bob, 1}], fun(#client{} = _Bob) -> 373: skip 374: end), 375: % we change password 376: NewPass = <<"niemakrolika">>, 377: {?NOCONTENT, _} = putt(admin, path("users", ["bob"]), 378: #{newpass => NewPass}), 379: % he logs with his alternative password 380: ConfigWithBobsAltPass = escalus_users:update_userspec(Config, bob, password, NewPass), 381: escalus:story(ConfigWithBobsAltPass, [{bob, 1}], fun(#client{} = _Bob) -> 382: ignore 383: end), 384: % we can't log with regular passwd anymore 385: try escalus:story(Config, [{bob, 1}], fun(Bob) -> ?PRT("Bob", Bob) end) of 386: _ -> ct:fail("bob connected with old password") 387: catch error:{badmatch, _} -> 388: ok 389: end, 390: % we change it back 391: {?NOCONTENT, _} = putt(admin, path("users", ["bob"]), 392: #{newpass => <<"makrolika">>}), 393: % now he logs again with the regular one 394: escalus:story(Config, [{bob, 1}], fun(#client{} = _Bob) -> 395: just_dont_do_anything 396: end), 397: % test invalid calls 398: Res1 = putt(admin, path("users", ["bob"]), 399: #{newpass => <<>>}), 400: {?BAD_REQUEST, <<"Empty password">>} = Res1, 401: Res2 = putt(admin, path("users", ["b@b"]), 402: #{newpass => NewPass}), 403: {?BAD_REQUEST, <<"Invalid JID">>} = Res2, 404: ok. 405: 406: list_contacts(Config) -> 407: escalus:fresh_story( 408: Config, [{alice, 1}, {bob, 1}], 409: fun(Alice, Bob) -> 410: AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)), 411: BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), 412: add_sample_contact(Bob, Alice), 413: % list bob's contacts 414: {?OK, R} = gett(admin, lists:flatten(["/contacts/", binary_to_list(BobJID)])), 415: [R1] = decode_maplist(R), 416: #{jid := AliceJID, subscription := <<"none">>, ask := <<"none">>} = R1, 417: ok 418: end 419: ), 420: ok. 421: 422: befriend_and_alienate(Config) -> 423: escalus:fresh_story( 424: Config, [{alice, 1}, {bob, 1}], 425: fun(Alice, Bob) -> 426: AliceJID = escalus_utils:jid_to_lower( 427: escalus_client:short_jid(Alice)), 428: BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), 429: AliceS = binary_to_list(AliceJID), 430: BobS = binary_to_list(BobJID), 431: AlicePath = lists:flatten(["/contacts/", AliceS]), 432: BobPath = lists:flatten(["/contacts/", BobS]), 433: % rosters are empty 434: check_roster_empty(AlicePath), 435: check_roster_empty(BobPath), 436: % adds them to rosters 437: {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), 438: {?NOCONTENT, _} = post(admin, BobPath, #{jid => AliceJID}), 439: check_roster(BobPath, AliceJID, none, none), 440: check_roster(AlicePath, BobJID, none, none), 441: % now do the subscription sequence 442: PutPathA = lists:flatten([AlicePath, "/", BobS]), 443: {?NOCONTENT, _} = putt(admin, PutPathA, #{action => <<"subscribe">>}), 444: check_roster(AlicePath, BobJID, none, out), 445: PutPathB = lists:flatten([BobPath, "/", AliceS]), 446: {?NOCONTENT, _} = putt(admin, PutPathB, #{action => <<"subscribed">>}), 447: check_roster(AlicePath, BobJID, to, none), 448: check_roster(BobPath, AliceJID, from, none), 449: {?NOCONTENT, _} = putt(admin, PutPathB, #{action => <<"subscribe">>}), 450: check_roster(BobPath, AliceJID, from, out), 451: {?NOCONTENT, _} = putt(admin, PutPathA, #{action => <<"subscribed">>}), 452: check_roster(AlicePath, BobJID, both, none), 453: check_roster(BobPath, AliceJID, both, none), 454: % now remove 455: {?NOCONTENT, _} = delete(admin, PutPathA), 456: check_roster_empty(AlicePath), 457: check_roster(BobPath, AliceJID, none, none), 458: {?NOCONTENT, _} = delete(admin, PutPathB), 459: check_roster_empty(BobPath), 460: APushes = lists:filter(fun escalus_pred:is_roster_set/1, 461: escalus:wait_for_stanzas(Alice, 20)), 462: AExp = [{none, none}, 463: {none, subscribe}, 464: {to, none}, 465: {both, none}, 466: {remove, none}], 467: check_pushlist(AExp, APushes), 468: BPushes = lists:filter(fun escalus_pred:is_roster_set/1, 469: escalus:wait_for_stanzas(Bob, 20)), 470: BExp = [{none, none}, 471: {from, none}, 472: {from, subscribe}, 473: {both, none}, 474: {to, none}, 475: {none, none}, 476: {remove, none}], 477: check_pushlist(BExp, BPushes), 478: ok 479: end 480: ), 481: ok. 482: 483: 484: befriend_and_alienate_auto(Config) -> 485: escalus:fresh_story( 486: Config, [{alice, 1}, {bob, 1}], 487: fun(Alice, Bob) -> 488: AliceJID = escalus_utils:jid_to_lower( 489: escalus_client:short_jid(Alice)), 490: BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), 491: AliceS = binary_to_list(AliceJID), 492: BobS = binary_to_list(BobJID), 493: AlicePath = lists:flatten(["/contacts/", AliceS]), 494: BobPath = lists:flatten(["/contacts/", BobS]), 495: check_roster_empty(AlicePath), 496: check_roster_empty(BobPath), 497: ManagePath = lists:flatten(["/contacts/", 498: AliceS, 499: "/", 500: BobS, 501: "/manage" 502: ]), 503: {?NOCONTENT, _} = putt(admin, ManagePath, #{action => <<"connect">>}), 504: check_roster(AlicePath, BobJID, both, none), 505: check_roster(BobPath, AliceJID, both, none), 506: {?NOCONTENT, _} = putt(admin, ManagePath, #{action => <<"disconnect">>}), 507: check_roster_empty(AlicePath), 508: check_roster_empty(BobPath), 509: APushes = lists:filter(fun escalus_pred:is_roster_set/1, 510: escalus:wait_for_stanzas(Alice, 20)), 511: ct:log("APushes: ~p", [APushes]), 512: AExp = [{none, none}, 513: {both, none}, 514: {remove, none}], 515: check_pushlist(AExp, APushes), 516: BPushes = lists:filter(fun escalus_pred:is_roster_set/1, 517: escalus:wait_for_stanzas(Bob, 20)), 518: ct:log("BPushes: ~p", [BPushes]), 519: BExp = [{none, none}, 520: {both, none}, 521: {remove, none}], 522: check_pushlist(BExp, BPushes), 523: ok 524: end 525: ), 526: ok. 527: 528: invalid_roster_operations(Config) -> 529: escalus:fresh_story( 530: Config, [{alice, 1}, {bob, 1}], 531: fun(Alice, Bob) -> 532: AliceJID = escalus_utils:jid_to_lower( 533: escalus_client:short_jid(Alice)), 534: BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), 535: AliceS = binary_to_list(AliceJID), 536: BobS = binary_to_list(BobJID), 537: AlicePath = lists:flatten(["/contacts/", AliceS]), 538: % adds them to rosters 539: {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = post(admin, AlicePath, #{jid => <<"@invalidjid">>}), 540: {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = post(admin, "/contacts/@invalid_jid", #{jid => BobJID}), 541: % it is idempotent 542: {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), 543: {?NOCONTENT, _} = post(admin, AlicePath, #{jid => BobJID}), 544: PutPathA = lists:flatten([AlicePath, "/@invalid_jid"]), 545: {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = putt(admin, PutPathA, #{action => <<"subscribe">>}), 546: PutPathB = lists:flatten(["/contacts/@invalid_jid/", BobS]), 547: {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = putt(admin, PutPathB, #{action => <<"subscribe">>}), 548: PutPathC = lists:flatten([AlicePath, "/", BobS]), 549: {?BAD_REQUEST, <<"invalid action">>} = putt(admin, PutPathC, #{action => <<"something stupid">>}), 550: ManagePath = lists:flatten(["/contacts/", 551: AliceS, 552: "/", 553: BobS, 554: "/manage" 555: ]), 556: {?BAD_REQUEST, <<"invalid action">>} = putt(admin, ManagePath, #{action => <<"off with his head">>}), 557: MangePathA = lists:flatten(["/contacts/", 558: "@invalid", 559: "/", 560: BobS, 561: "/manage" 562: ]), 563: {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = putt(admin, MangePathA, #{action => <<"connect">>}), 564: MangePathB = lists:flatten(["/contacts/", 565: AliceS, 566: "/", 567: "@bzzz", 568: "/manage" 569: ]), 570: {?BAD_REQUEST, <<"Invalid jid", _/binary>>} = putt(admin, MangePathB, #{action => <<"connect">>}), 571: ok 572: end 573: ). 574: 575: types_are_checked_separately_for_args_and_return(Config) -> 576: escalus:story( 577: Config, [{alice, 1}], 578: fun(_Alice) -> 579: % argument doesn't pass typecheck 580: {?BAD_REQUEST, _} = post(admin, "/test_arg", #{arg => 1}), 581: % return value doesn't pass typecheck 582: {?ERROR, _} = post(admin, "/test_return", #{arg => true}), 583: ok 584: end 585: ). 586: 587: %%-------------------------------------------------------------------- 588: %% Helpers 589: %%-------------------------------------------------------------------- 590: 591: send_messages(Alice, Bob) -> 592: AliceJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)), 593: BobJID = escalus_utils:jid_to_lower(escalus_client:short_jid(Bob)), 594: M = #{caller => BobJID, to => AliceJID, body => <<"hello from Bob">>}, 595: {?NOCONTENT, _} = post(admin, <<"/messages">>, M), 596: M1 = #{caller => AliceJID, to => BobJID, body => <<"hello from Alice">>}, 597: {?NOCONTENT, _} = post(admin, <<"/messages">>, M1), 598: {M, M1}. 599: 600: send_message_bin(BFrom, BTo) -> 601: % this is to trigger invalid jid errors 602: M = #{caller => BFrom, to => BTo, body => <<"whatever">>}, 603: post(admin, <<"/messages">>, M). 604: 605: send_extended_message(From, To) -> 606: M = #xmlel{name = <<"message">>, 607: attrs = [{<<"from">>, escalus_client:full_jid(From)}, 608: {<<"to">>, escalus_client:full_jid(To)}, 609: {<<"extra">>, <<"attribute">>}], 610: children = [#xmlel{name = <<"body">>, 611: children = [#xmlcdata{content = <<"the body">>}]}, 612: #xmlel{name = <<"sibling">>, 613: children = [#xmlcdata{content = <<"inside the sibling">>}]} 614: ] 615: }, 616: M1 = #{stanza => exml:to_binary(M)}, 617: {?NOCONTENT, _} = post(admin, <<"/stanzas">>, M1), 618: ok. 619: 620: send_flawed_stanza(missing_attribute, From, _To) -> 621: M = #xmlel{name = <<"message">>, 622: attrs = [{<<"from">>, escalus_client:full_jid(From)}, 623: {<<"extra">>, <<"attribute">>}], 624: children = [#xmlel{name = <<"body">>, 625: children = [#xmlcdata{content = <<"the body">>}]}, 626: #xmlel{name = <<"sibling">>, 627: children = [#xmlcdata{content = <<"inside the sibling">>}]} 628: ] 629: }, 630: ct:log("M: ~p", [M]), 631: M1 = #{stanza => exml:to_binary(M)}, 632: post(admin, <<"/stanzas">>, M1); 633: send_flawed_stanza(malformed_xml, _From, _To) -> 634: % closing > is missing 635: BadStanza = <<"<message from='alicE@localhost/res1' to='bOb@localhost/res1'><body>the body</body></message">>, 636: post(admin, <<"/stanzas">>, #{stanza => BadStanza}). 637: 638: 639: check_roster(Path, Jid, Subs, Ask) -> 640: {?OK, R} = gett(admin, Path), 641: S = atom_to_binary(Subs, latin1), 642: A = atom_to_binary(Ask, latin1), 643: Res = decode_maplist(R), 644: [#{jid := Jid, subscription := S, ask := A}] = Res. 645: 646: check_roster_empty(Path) -> 647: {?OK, R} = gett(admin, Path), 648: [] = decode_maplist(R). 649: 650: get_messages(Me, Other, Count) -> 651: GetPath = lists:flatten(["/messages/", 652: binary_to_list(Me), 653: "/", binary_to_list(Other), 654: "?limit=", integer_to_list(Count)]), 655: {?OK, Msgs} = gett(admin, GetPath), 656: Msgs. 657: 658: get_messages(Me, Other, Before, Count) -> 659: GetPath = lists:flatten(["/messages/", 660: binary_to_list(Me), 661: "/", binary_to_list(Other), 662: "?before=", integer_to_list(Before), 663: "&limit=", integer_to_list(Count)]), 664: {?OK, Msgs} = gett(admin, GetPath), 665: Msgs. 666: 667: stop_start_command_module(_) -> 668: %% Precondition: module responsible for resource is started. If we 669: %% stop the module responsible for this resource then the same 670: %% test will fail. If we start the module responsible for this 671: %% resource then the same test will succeed. With the precondition 672: %% described above we test both transition from `started' to 673: %% `stopped' and from `stopped' to `started'. 674: {?OK, _} = gett(admin, <<"/commands">>), 675: {stopped, _} = dynamic_modules:stop(host_type(), mod_commands), 676: {?NOT_FOUND, _} = gett(admin, <<"/commands">>), 677: {started, _} = dynamic_modules:start(host_type(), mod_commands, []), 678: timer:sleep(200), %% give the server some time to build the paths again 679: {?OK, _} = gett(admin, <<"/commands">>). 680: 681: to_list(V) when is_binary(V) -> 682: binary_to_list(V); 683: to_list(V) when is_list(V) -> 684: V. 685: 686: add_sample_contact(Bob, Alice) -> 687: escalus:send(Bob, escalus_stanza:roster_add_contact(Alice, 688: [<<"friends">>], 689: <<"Alicja">>)), 690: Received = escalus:wait_for_stanzas(Bob, 2), 691: escalus:assert_many([is_roster_set, is_iq_result], Received), 692: Result = hd([R || R <- Received, escalus_pred:is_roster_set(R)]), 693: escalus:assert(count_roster_items, [1], Result), 694: escalus:send(Bob, escalus_stanza:iq_result(Result)). 695: 696: check_pushlist([], _Stanzas) -> 697: ok; 698: check_pushlist(Expected, []) -> 699: ?assertEqual(Expected, []); 700: check_pushlist(Expected, [Iq|StanzaTail]) -> 701: [{ExpectedSub, ExpectedAsk}| TailExp] = Expected, 702: case does_push_match(Iq, ExpectedSub, ExpectedAsk) of 703: true -> 704: check_pushlist(TailExp, StanzaTail); 705: false -> 706: check_pushlist(Expected, StanzaTail) 707: end. 708: 709: does_push_match(Iq, ExpectedSub, ExpectedAsk) -> 710: [Subs] = exml_query:paths(Iq, [{element, <<"query">>}, 711: {element, <<"item">>}, 712: {attr, <<"subscription">>}]), 713: AskList = exml_query:paths(Iq, [{element, <<"query">>}, 714: {element, <<"item">>}, 715: {attr, <<"ask">>}]), 716: Ask = case AskList of 717: [] -> <<"none">>; 718: [A] -> A 719: end, 720: ESub = atom_to_binary(ExpectedSub, latin1), 721: EAsk = atom_to_binary(ExpectedAsk, latin1), 722: {Subs, Ask} == {ESub, EAsk}. 723: 724: path(Category) -> 725: path(Category, []). 726: 727: path(Category, Items) -> 728: DomainStr = binary_to_list(domain()), 729: string:join(["", Category, DomainStr | Items], "/").