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