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