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], "/").