1: %%==============================================================================
    2: %% Copyright 2013 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: -module(mongooseimctl_SUITE).
   17: -compile([export_all, nowarn_export_all, nowarn_shadow_vars]).
   18: 
   19: -include_lib("escalus/include/escalus.hrl").
   20: -include_lib("common_test/include/ct.hrl").
   21: -include_lib("exml/include/exml.hrl").
   22: -include_lib("eunit/include/eunit.hrl").
   23: 
   24: -import(mongooseimctl_helper, [mongooseimctl/3, rpc_call/3]).
   25: -import(mongoose_helper, [auth_modules/0]).
   26: -import(distributed_helper, [mim/0,
   27:                              require_rpc_nodes/1,
   28:                              rpc/4]).
   29: -import(domain_helper, [host_type/0, domain/0]).
   30: 
   31: -define(HTTP_UPLOAD_FILENAME, "tmp.txt").
   32: -define(HTTP_UPLOAD_FILESIZE, "5").
   33: -define(HTTP_UPLOAD_TIMEOUT, "666").
   34: -define(HTTP_UPLOAD_PARAMS(ContentType), ?HTTP_UPLOAD_PARAMS(?HTTP_UPLOAD_FILENAME,
   35:                                                              ?HTTP_UPLOAD_FILESIZE,
   36:                                                              ContentType,
   37:                                                              ?HTTP_UPLOAD_TIMEOUT)).
   38: -define(HTTP_UPLOAD_PARAMS_WITH_FILESIZE(X), ?HTTP_UPLOAD_PARAMS(?HTTP_UPLOAD_FILENAME, X,
   39:                                                                  "", ?HTTP_UPLOAD_TIMEOUT)).
   40: -define(HTTP_UPLOAD_PARAMS_WITH_TIMEOUT(X), ?HTTP_UPLOAD_PARAMS(?HTTP_UPLOAD_FILENAME,
   41:                                                                 ?HTTP_UPLOAD_FILESIZE, "", X)).
   42: -define(HTTP_UPLOAD_PARAMS(FileName, FileSize, ContentType, Timeout),
   43:     [domain(), FileName, FileSize, ContentType, Timeout]).
   44: 
   45: -define(CTL_ERROR(Messsage), "Error: \"" ++ Messsage ++ "\"\n").
   46: -define(HTTP_UPLOAD_NOT_ENABLED_ERROR, ?CTL_ERROR("mod_http_upload is not loaded for this host")).
   47: -define(HTTP_UPLOAD_FILESIZE_ERROR, ?CTL_ERROR("size must be positive integer")).
   48: -define(HTTP_UPLOAD_TIMEOUT_ERROR, ?CTL_ERROR("timeout must be positive integer")).
   49: 
   50: -define(S3_BUCKET_URL, "http://localhost:9000/mybucket/").
   51: -define(S3_REGION, "eu-east-25").
   52: -define(S3_ACCESS_KEY_ID, "AKIAIAOAONIULXQGMOUA").
   53: -define(MINIO_OPTS(AddAcl),
   54:     [
   55:         {max_file_size, 1234},
   56:         {s3, [
   57:             {bucket_url, ?S3_BUCKET_URL},
   58:             {add_acl, AddAcl},
   59:             {region, ?S3_REGION},
   60:             {access_key_id, ?S3_ACCESS_KEY_ID},
   61:             {secret_access_key, "CG5fGqG0/n6NCPJ10FylpdgRnuV52j8IZvU7BSj8"}
   62:         ]}
   63:     ]).
   64: 
   65: %%Prefix MUST be a constant string, otherwise it results in compilation error
   66: -define(GET_URL(Prefix, Sting), fun() -> Prefix ++ URL = Sting, URL end()).
   67: 
   68: %% The following is an example presigned URL:
   69: %%
   70: %%     https://s3.amazonaws.com/examplebucket/test.txt
   71: %%     ?X-Amz-Algorithm=AWS4-HMAC-SHA256
   72: %%     &X-Amz-Credential=<your-access-key-id>/20130721/us-east-1/s3/aws4_request
   73: %%     &X-Amz-Date=20130721T201207Z
   74: %%     &X-Amz-Expires=86400
   75: %%     &X-Amz-SignedHeaders=host
   76: %%     &X-Amz-Signature=<signature-value>
   77: %%
   78: %% for more details see
   79: %%     https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
   80: -define(S3_BASE_URL_REGEX, "^"?S3_BUCKET_URL".+/"?HTTP_UPLOAD_FILENAME).
   81: -define(S3_ALGORITHM_REGEX, "[?&]X-Amz-Algorithm=AWS4-HMAC-SHA256(&|$)").
   82: -define(S3_CREDENTIAL_REGEX,
   83:         % X-Amz-Credential=<your-access-key-id>/<date>/<AWS-region>/<AWS-service>/aws4_request
   84:         "[?&]X-Amz-Credential="?S3_ACCESS_KEY_ID"%2F[0-9]{8}%2F"?S3_REGION"%2Fs3%2Faws4_request(&|$)").
   85: -define(S3_DATE_REGEX, "X-Amz-Date=[0-9]{8}T[0-9]{6}Z(&|$)").
   86: -define(S3_EXPIRATION_REGEX, "[?&]X-Amz-Expires="?HTTP_UPLOAD_TIMEOUT"(&|$)").
   87: -define(S3_SIGNED_HEADERS, "[?&]X-Amz-SignedHeaders=content-length%3Bhost(&|$)").
   88: -define(S3_SIGNED_HEADERS_WITH_ACL,
   89:         "[?&]X-Amz-SignedHeaders=content-length%3Bhost%3Bx-amz-acl(&|$)").
   90: -define(S3_SIGNED_HEADERS_WITH_CONTENT_TYPE,
   91:         "[?&]X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost(&|$)").
   92: -define(S3_SIGNED_HEADERS_WITH_CONTENT_TYPE_AND_ACL,
   93:         "[?&]X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost%3Bx-amz-acl(&|$)").
   94: -define(S3_SIGNATURE_REGEX, "[?&]X-Amz-Signature=[^&]+(&|$)").
   95: 
   96: 
   97: -record(offline_msg, {us, timestamp, expire, from, to, packet, permanent_fields = []}).
   98: 
   99: %%--------------------------------------------------------------------
  100: %% Suite configuration
  101: %%--------------------------------------------------------------------
  102: 
  103: all() ->
  104:     [
  105:      {group, accounts},
  106:      {group, sessions},
  107:      {group, vcard},
  108:      {group, roster},
  109:      {group, roster_advanced},
  110:      {group, last},
  111:      {group, private},
  112:      {group, stanza},
  113:      {group, stats},
  114:      {group, basic},
  115:      {group, upload},
  116:      {group, graphql}
  117:     ].
  118: 
  119: groups() ->
  120:     [{accounts, [sequence], accounts()},
  121:      {sessions, [sequence], sessions()},
  122:      {vcard, [sequence], vcard()},
  123:      {roster, [sequence], roster()},
  124:      {last, [sequence], last()},
  125:      {private, [sequence], private()},
  126:      {stanza, [sequence], stanza()},
  127:      {roster_advanced, [sequence], roster_advanced()},
  128:      {basic, [sequence], basic()},
  129:      {stats, [sequence], stats()},
  130:      {upload, [], upload()},
  131:      {upload_with_acl, [], upload_enabled()},
  132:      {upload_without_acl, [], upload_enabled()},
  133:      {graphql, [], graphql()}].
  134: 
  135: basic() ->
  136:     [simple_register,
  137:      simple_unregister,
  138:      register_twice,
  139:      backup_restore_mnesia,
  140:      restore_mnesia_wrong,
  141:      dump_and_load,
  142:      load_mnesia_wrong,
  143:      dump_table,
  144:      get_loglevel,
  145:      remove_old_messages_test,
  146:      remove_expired_messages_test].
  147: 
  148: accounts() -> [change_password, check_password_hash, check_password,
  149:                check_account, ban_account, num_active_users, delete_old_users,
  150:                delete_old_users_vhost].
  151: 
  152: sessions() -> [num_resources_num, kick_session, status,
  153:                sessions_info, set_presence].
  154: 
  155: vcard() -> [vcard_rw, vcard2_rw, vcard2_multi_rw].
  156: 
  157: roster() -> [rosteritem_rw,
  158:              presence_after_add_rosteritem,
  159:              push_roster,
  160:              push_roster_all,
  161:              push_roster_alltoall].
  162: 
  163: roster_advanced() -> [process_rosteritems_list_simple,
  164:                       process_rosteritems_list_nomatch,
  165:                       process_rosteritems_list_advanced1,
  166:                       process_rosteritems_list_advanced2,
  167:                       process_rosteritems_delete_advanced,
  168:                       process_rosteritems_delete_advanced2].
  169: 
  170: last() -> [set_last].
  171: 
  172: private() -> [private_rw].
  173: 
  174: stanza() -> [send_message, send_message_wrong_jid, send_stanza, send_stanzac2s_wrong].
  175: 
  176: stats() -> [stats_global, stats_host].
  177: 
  178: upload() ->
  179:     [upload_not_enabled, upload_wrong_filesize, upload_wrong_timeout,
  180:      {group, upload_with_acl}, {group, upload_without_acl}].
  181: 
  182: upload_enabled() ->
  183:     [upload_returns_correct_urls_without_content_type,
  184:      upload_returns_correct_urls_with_content_type,
  185:      real_upload_without_content_type,
  186:      real_upload_with_content_type].
  187: 
  188: graphql() ->
  189:     [graphql_wrong_arguments_number,
  190:      can_execute_admin_queries_with_permissions,
  191:      can_handle_execution_error].
  192: 
  193: suite() ->
  194:     require_rpc_nodes([mim]) ++ escalus:suite().
  195: 
  196: init_per_suite(Config) ->
  197:     TemplatePath = filename:join(?config(mim_data_dir, Config), "roster.template"),
  198:     AuthMods = auth_modules(),
  199:     Node = mim(),
  200:     Config1 = ejabberd_node_utils:init(Node, Config),
  201:     Config2 = escalus:init_per_suite([{ctl_auth_mods, AuthMods},
  202:                                       {roster_template, TemplatePath} | Config1]),
  203:     dynamic_modules:ensure_modules(domain_helper:host_type(), [{mod_last,
  204:         config_parser_helper:default_mod_config(mod_last)}]),
  205:     dynamic_modules:ensure_modules(domain_helper:secondary_host_type(),
  206:         [{mod_last, config_parser_helper:default_mod_config(mod_last)}]),
  207:     prepare_roster_template(TemplatePath, domain()),
  208:     %% dump_and_load requires at least one mnesia table
  209:     %% ensure, that passwd table is available
  210:     catch rpc_call(ejabberd_auth_internal, start, [host_type()]),
  211:     escalus:create_users(Config2, escalus:get_users([alice, mike, bob, kate])).
  212: 
  213: prepare_roster_template(TemplatePath, Domain) ->
  214:     {ok, [RosterIn]} = file:consult(TemplatePath ++ ".in"),
  215:     DomainStr = binary_to_list(Domain),
  216:     Roster = [{User, DomainStr, Group, Name} || {User, Group, Name} <- RosterIn],
  217:     FormattedRoster = io_lib:format("~tp.~n", [Roster]),
  218:     file:write_file(TemplatePath, FormattedRoster).
  219: 
  220: end_per_suite(Config) ->
  221:     Config1 = lists:keydelete(ctl_auth_mods, 1, Config),
  222:     delete_users(Config1),
  223:     dynamic_modules:stop(domain_helper:host_type(), mod_last),
  224:     dynamic_modules:stop(domain_helper:secondary_host_type(), mod_last),
  225:     escalus:end_per_suite(Config1).
  226: 
  227: init_per_group(basic, Config) ->
  228:     dynamic_modules:ensure_modules(domain_helper:host_type(), [{mod_offline, []}]),
  229:     Config;
  230: init_per_group(private, Config) ->
  231:     dynamic_modules:ensure_modules(domain_helper:host_type(),
  232:                                    [{mod_private, #{iqdisc => one_queue}}]
  233:                                   ),
  234:     Config;
  235: init_per_group(vcard, Config) ->
  236:     case rpc(mim(), gen_mod, get_module_opt, [host_type(), mod_vcard, backend, mnesia]) of
  237:         ldap ->
  238:             {skip, vcard_set_not_supported_with_ldap};
  239:         _ ->
  240:             Config
  241:     end;
  242: init_per_group(roster_advanced, Config) ->
  243:     case rpc(mim(), gen_mod, get_module_opt, [host_type(), mod_roster, backend, mnesia]) of
  244:         mnesia ->
  245:             Config;
  246:         _ ->
  247:             {skip, command_process_rosteritems_supports_only_mnesia}
  248:     end;
  249: init_per_group(upload_without_acl, Config) ->
  250:     dynamic_modules:start(host_type(),  mod_http_upload, ?MINIO_OPTS(false)),
  251:     [{with_acl, false} | Config];
  252: init_per_group(upload_with_acl, Config) ->
  253:     dynamic_modules:start(host_type(), mod_http_upload, ?MINIO_OPTS(true)),
  254:     [{with_acl, true} | Config];
  255: init_per_group(_GroupName, Config) ->
  256:     Config.
  257: 
  258: end_per_group(basic, Config) ->
  259:     dynamic_modules:stop(domain_helper:host_type(), mod_offline),
  260:     Config;
  261: end_per_group(private, Config) ->
  262:     dynamic_modules:stop(domain_helper:host_type(), mod_private),
  263:     Config;
  264: end_per_group(Rosters, Config) when (Rosters == roster) or (Rosters == roster_advanced) ->
  265:     TemplatePath = escalus_config:get_config(roster_template, Config),
  266:     RegUsers = [atom_to_list(U) || {U, _} <- escalus_config:get_config(escalus_users, Config)],
  267:     {ok, [Roster]} = file:consult(TemplatePath),
  268:     C = fun({U, S, _, _}) ->
  269:         case lists:member(U, RegUsers) of
  270:             true ->
  271:                 SB = string_to_binary(S),
  272:                 UB = string_to_binary(U),
  273:                 Acc = mongoose_helper:new_mongoose_acc(SB),
  274:                 rpc(mim(), mongoose_hooks, remove_user, [Acc, SB, UB]);
  275:             _ ->
  276:                ok
  277:         end
  278:     end,
  279:     lists:foreach(C, Roster),
  280:     Config;
  281: end_per_group(UploadGroup, Config) when UploadGroup =:= upload_without_acl;
  282:                                         UploadGroup =:= upload_with_acl ->
  283:     dynamic_modules:stop(host_type(), mod_http_upload),
  284:     Config;
  285: end_per_group(_GroupName, Config) ->
  286:     Config.
  287: 
  288: get_registered_users() ->
  289:     rpc(mim(), ejabberd_auth, get_vh_registered_users, [domain()]).
  290: 
  291: init_per_testcase(CaseName, Config)
  292:   when CaseName == delete_old_users_vhost
  293:        orelse CaseName == stats_global
  294:        orelse CaseName == stats_host ->
  295:     {_, AuthMods} = lists:keyfind(ctl_auth_mods, 1, Config),
  296:     case lists:member(ejabberd_auth_ldap, AuthMods) of
  297:         true -> {skip, "not supported for LDAP"};
  298:         false -> escalus:init_per_testcase(CaseName, Config)
  299:     end;
  300: init_per_testcase(check_password_hash, Config) ->
  301:     {_, AuthMods} = lists:keyfind(ctl_auth_mods, 1, Config),
  302:     case lists:member(ejabberd_auth_ldap, AuthMods) of
  303:         true ->
  304:             {skip, not_fully_supported_with_ldap};
  305:         false ->
  306:             AuthOpts = mongoose_helper:auth_opts_with_password_format(plain),
  307:             Config1 = mongoose_helper:backup_and_set_config_option(Config, {auth, host_type()},
  308:                                                                    AuthOpts),
  309:             Config2 = escalus:create_users(Config1, escalus:get_users([carol])),
  310:             escalus:init_per_testcase(check_password_hash, Config2)
  311:     end;
  312: init_per_testcase(delete_old_users, Config) ->
  313:     {_, AuthMods} = lists:keyfind(ctl_auth_mods, 1, Config),
  314:     case lists:member(ejabberd_auth_ldap, AuthMods) of
  315:         true -> {skip, not_fully_supported_with_ldap};
  316:         false -> escalus:init_per_testcase(delete_old_users, Config)
  317:     end;
  318: init_per_testcase(CaseName, Config) when CaseName == real_upload_without_content_type;
  319:                                          CaseName == real_upload_with_content_type ->
  320:     case mongoose_helper:should_minio_be_running(Config) of
  321:         true -> escalus:init_per_testcase(CaseName, Config);
  322:         false -> {skip, "minio is not running"}
  323:     end;
  324: init_per_testcase(CaseName, Config) ->
  325:     escalus:init_per_testcase(CaseName, Config).
  326: 
  327: end_per_testcase(delete_old_users, Config) ->
  328:     Users = escalus_users:get_users([alice, bob, kate, mike]),
  329:     lists:foreach(fun({_User, UserSpec}) ->
  330:                 {Username, Domain, Pass} = get_user_data(UserSpec, Config),
  331:                 JID = mongoose_helper:make_jid(Username, Domain),
  332:                 rpc(mim(), ejabberd_auth, try_register, [JID, Pass])
  333:         end, Users),
  334:     escalus:end_per_testcase(delete_old_users, Config);
  335: end_per_testcase(check_password_hash, Config) ->
  336:     mongoose_helper:restore_config(Config),
  337:     escalus:delete_users(Config, escalus:get_users([carol]));
  338: end_per_testcase(CaseName, Config) ->
  339:     %% Because kick_session fails with unexpected stanza received:
  340:     %% <presence from="alicE@localhost/res3"
  341:     %%     to="alice@localhost/res1" type="unavailable" />
  342:     %% TODO: Remove when escalus learns how to automatically deal
  343:     %% with 'unavailable' stanzas on client stop.
  344:     mongoose_helper:kick_everyone(),
  345:     escalus:end_per_testcase(CaseName, Config).
  346: 
  347: %%--------------------------------------------------------------------
  348: %% http upload tests
  349: %%--------------------------------------------------------------------
  350: upload_not_enabled(Config) ->
  351:     Ret = mongooseimctl("http_upload", ?HTTP_UPLOAD_PARAMS("text/plain"), Config),
  352:     ?assertEqual({?HTTP_UPLOAD_NOT_ENABLED_ERROR, 1}, Ret).
  353: 
  354: upload_wrong_filesize(Config) ->
  355:     Ret = mongooseimctl("http_upload", ?HTTP_UPLOAD_PARAMS_WITH_FILESIZE("0"), Config),
  356:     ?assertEqual({?HTTP_UPLOAD_FILESIZE_ERROR, 1}, Ret),
  357:     Ret = mongooseimctl("http_upload", ?HTTP_UPLOAD_PARAMS_WITH_FILESIZE("-1"), Config),
  358:     ?assertEqual({?HTTP_UPLOAD_FILESIZE_ERROR, 1}, Ret).
  359: 
  360: upload_wrong_timeout(Config) ->
  361:     Ret = mongooseimctl("http_upload", ?HTTP_UPLOAD_PARAMS_WITH_TIMEOUT("0"), Config),
  362:     ?assertEqual({?HTTP_UPLOAD_TIMEOUT_ERROR, 1}, Ret),
  363:     Ret = mongooseimctl("http_upload", ?HTTP_UPLOAD_PARAMS_WITH_TIMEOUT("-1"), Config),
  364:     ?assertEqual({?HTTP_UPLOAD_TIMEOUT_ERROR, 1}, Ret).
  365: 
  366: upload_returns_correct_urls_with_content_type(Config) ->
  367:     upload_returns_correct_urls(Config, "text/plain").
  368: 
  369: upload_returns_correct_urls_without_content_type(Config) ->
  370:     upload_returns_correct_urls(Config, "").
  371: 
  372: real_upload_with_content_type(Config) ->
  373:     real_upload(Config, "text/plain").
  374: 
  375: real_upload_without_content_type(Config) ->
  376:     real_upload(Config, "").
  377: 
  378: upload_returns_correct_urls(Config, ContentType) ->
  379:     HttpUploadParams = ?HTTP_UPLOAD_PARAMS(ContentType),
  380:     {Output, 0} = mongooseimctl("http_upload", HttpUploadParams, Config),
  381:     {PutURL, GetURL} = get_urls(Output),
  382:     WithACL = proplists:get_value(with_acl, Config),
  383:     check_urls(PutURL, GetURL, WithACL, ContentType).
  384: 
  385: get_urls(Output) ->
  386:     [PutStr, GetStr | _] = string:split(Output, "\n", all),
  387:     PutURL = ?GET_URL("PutURL: ", PutStr),
  388:     GetURL = ?GET_URL("GetURL: ", GetStr),
  389:     {PutURL, GetURL}.
  390: 
  391: check_urls(PutURL, GetURL, WithACL, ContentType) ->
  392:     check_bucket_url_and_filename(put, PutURL),
  393:     check_bucket_url_and_filename(get, GetURL),
  394:     check_substring(?S3_ALGORITHM_REGEX, PutURL),
  395:     check_substring(?S3_CREDENTIAL_REGEX, PutURL),
  396:     check_substring(?S3_DATE_REGEX, PutURL),
  397:     check_substring(?S3_EXPIRATION_REGEX, PutURL),
  398:     SignedHeadersRegex = signed_headers_regex(WithACL, ContentType),
  399:     check_substring(SignedHeadersRegex, PutURL),
  400:     check_substring(?S3_SIGNATURE_REGEX, PutURL).
  401: 
  402: check_bucket_url_and_filename(Type, Url) ->
  403:     UrlRegex = case Type of
  404:                    get -> ?S3_BASE_URL_REGEX"$";
  405:                    put -> ?S3_BASE_URL_REGEX"\?.*"
  406:                end,
  407:     ?assertMatch({match, [{0, _}]}, re:run(Url, UrlRegex)).
  408: 
  409: check_substring(SubString, String) ->
  410:     ?assertMatch({match, [_]}, re:run(String, SubString, [global])).
  411: 
  412: signed_headers_regex(false, "") -> ?S3_SIGNED_HEADERS;
  413: signed_headers_regex(false, _)  -> ?S3_SIGNED_HEADERS_WITH_CONTENT_TYPE;
  414: signed_headers_regex(true, "")  -> ?S3_SIGNED_HEADERS_WITH_ACL;
  415: signed_headers_regex(true, _)   -> ?S3_SIGNED_HEADERS_WITH_CONTENT_TYPE_AND_ACL.
  416: 
  417: real_upload(Config, ContentType) ->
  418:     #{node := Node} = mim(),
  419:     BinPath = distributed_helper:bin_path(Node, Config),
  420:     UploadScript = filename:join(?config(mim_data_dir, Config), "test_file_upload.sh"),
  421:     Ret = mongooseimctl_helper:run(UploadScript, [ContentType], [{cd, BinPath}]),
  422:     ?assertMatch({_, 0}, Ret),
  423:     ok.
  424: %%--------------------------------------------------------------------
  425: %% mod_admin_extra_accounts tests
  426: %%--------------------------------------------------------------------
  427: 
  428: change_password(Config) ->
  429:     {User, Domain, OldPassword} = get_user_data(alice, Config),
  430:     mongooseimctl("change_password", [User, Domain, <<OldPassword/binary, $2>>], Config),
  431:     {error, {connection_step_failed, _, _}} = escalus_client:start_for(Config, alice, <<"newres">>),
  432:     mongooseimctl("change_password", [User, Domain, OldPassword], Config),
  433:     {ok, Alice2} = escalus_client:start_for(Config, alice, <<"newres2">>),
  434:     escalus_client:stop(Config, Alice2).
  435: 
  436: check_password_hash(Config) ->
  437:     {User, Domain, Pass} = get_user_data(carol, Config),
  438:     MD5Hash = get_md5(Pass),
  439:     MD5HashBad = get_md5(<<Pass/binary, "bad">>),
  440:     SHAHash = get_sha(Pass),
  441: 
  442:     {_, 0} = mongooseimctl("check_password_hash", [User, Domain, MD5Hash, "md5"], Config),
  443:     {_, ErrCode} = mongooseimctl("check_password_hash", [User, Domain, MD5HashBad, "md5"], Config),
  444:     true = (ErrCode =/= 0), %% Must return code other than 0
  445:     {_, 0} = mongooseimctl("check_password_hash", [User, Domain, SHAHash, "sha"], Config).
  446: 
  447: check_password(Config) ->
  448:     {User, Domain, Pass} = get_user_data(alice, Config),
  449: 
  450:     {_, 0} = mongooseimctl("check_password", [User, Domain, Pass], Config),
  451:     {_, ErrCode} = mongooseimctl("check_password", [User, Domain, <<Pass/binary, "Bad">>], Config),
  452:     true = (ErrCode =/= 0). %% Must return code other than 0
  453: 
  454: check_account(Config) ->
  455:     {User, Domain, _Pass} = get_user_data(alice, Config),
  456: 
  457:     {_, 0} = mongooseimctl("check_account", [User, Domain], Config),
  458:     {_, ErrCode} = mongooseimctl("check_account", [<<User/binary, "Bad">>, Domain], Config),
  459:     true = (ErrCode =/= 0). %% Must return code other than 0
  460: 
  461: ban_account(Config) ->
  462:     {User, Domain, Pass} = get_user_data(mike, Config),
  463: 
  464:     {ok, Mike} = escalus_client:start_for(Config, mike, <<"newres">>),
  465:     {_, 0} = mongooseimctl("ban_account", [User, Domain, "SomeReason"], Config),
  466:     escalus:assert(is_stream_error, [<<"conflict">>, <<"SomeReason">>],
  467:                    escalus:wait_for_stanza(Mike)),
  468:     {error, {connection_step_failed, _, _}} = escalus_client:start_for(Config, mike, <<"newres2">>),
  469:     mongooseimctl("change_password", [User, Domain, Pass], Config),
  470:     escalus_connection:wait_for_close(Mike, 1000),
  471:     escalus_cleaner:remove_client(Config, Mike).
  472: 
  473: num_active_users(Config) ->
  474:     %% Given some users with recorded last activity timestamps
  475:     {AliceName, Domain, _} = get_user_data(alice, Config),
  476:     {MikeName, Domain, _} = get_user_data(mike, Config),
  477:     {Mega, Secs, _} = os:timestamp(),
  478:     Now = Mega * 1000000 + Secs,
  479:     set_last(AliceName, Domain, Now),
  480:     set_last(MikeName, Domain, Now),
  481:     {SLastActiveBefore, _} = mongooseimctl("num_active_users", [Domain, "5"], Config),
  482:     %% When we artificially remove a user's last activity timestamp in the given period
  483:     TenDaysAgo = Now - 864000,
  484:     set_last(MikeName, Domain, TenDaysAgo),
  485:     %% Then we expect that the number of active users in the last 5 days is one less
  486:     %% than before the change above
  487:     {SLastActiveAfter, _} = mongooseimctl("num_active_users", [Domain, "5"], Config),
  488:     NLastActiveBefore = list_to_integer(string:strip(SLastActiveBefore, both, $\n)),
  489:     NLastActiveAfter = list_to_integer(string:strip(SLastActiveAfter, both, $\n)),
  490:     NLastActiveAfter = NLastActiveBefore - 1.
  491: 
  492: delete_old_users(Config) ->
  493:     {AliceName, Domain, _} = get_user_data(alice, Config),
  494:     {BobName, Domain, _} = get_user_data(bob, Config),
  495:     {KateName, Domain, _} = get_user_data(kate, Config),
  496:     {MikeName, Domain, _} = get_user_data(mike, Config),
  497: 
  498:     {Mega, Secs, _} = os:timestamp(),
  499:     Now = Mega*1000000+Secs,
  500:     set_last(AliceName, Domain, Now),
  501:     set_last(BobName, Domain, Now),
  502:     set_last(MikeName, Domain, Now),
  503:     set_last(KateName, Domain, 0),
  504: 
  505:     {_, 0} = mongooseimctl("delete_old_users", ["10"], Config),
  506:     {_, 0} = mongooseimctl("check_account", [AliceName, Domain], Config),
  507:     {_, ErrCode} = mongooseimctl("check_account", [KateName, Domain], Config),
  508:     true = (ErrCode =/= 0). %% Must return code other than 0
  509: 
  510: delete_old_users_vhost(Config) ->
  511:     {AliceName, Domain, _} = get_user_data(alice, Config),
  512:     {KateName, Domain, KatePass} = get_user_data(kate, Config),
  513:     SecDomain = ct:get_config({hosts, mim, secondary_domain}),
  514: 
  515:     {Mega, Secs, _} = os:timestamp(),
  516:     Now = Mega*1000000+Secs,
  517:     set_last(AliceName, Domain, Now-86400*30),
  518: 
  519:     {_, 0} = mongooseimctl("register_identified", [KateName, SecDomain, KatePass], Config),
  520:     {_, 0} = mongooseimctl("check_account", [KateName, SecDomain], Config),
  521:     {_, 0} = mongooseimctl("delete_old_users_vhost", [SecDomain, "10"], Config),
  522:     {_, 0} = mongooseimctl("check_account", [AliceName, Domain], Config),
  523:     {_, ErrCode} = mongooseimctl("check_account", [KateName, SecDomain], Config),
  524:     true = (ErrCode =/= 0). %% Must return code other than 0
  525: 
  526: %%--------------------------------------------------------------------
  527: %% mod_admin_extra_accounts tests
  528: %%--------------------------------------------------------------------
  529: 
  530: %% Checks both num_resources and resource_num
  531: num_resources_num(Config) ->
  532:     escalus:story(Config, [{alice, 3}, {bob, 1}], fun(_, Alice2, _, _) ->
  533:                 {Username, Domain, _} = get_user_data(alice, Config),
  534:                 ResName = binary_to_list(escalus_client:resource(Alice2)) ++ "\n",
  535: 
  536:                 {"3\n", _} = mongooseimctl("num_resources", [Username, Domain], Config),
  537:                 {ResName, _} = mongooseimctl("resource_num", [Username, Domain, "2"], Config)
  538:         end).
  539: 
  540: kick_session(Config) ->
  541:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  542:                 Username = escalus_client:username(Alice),
  543:                 Domain = escalus_client:server(Alice),
  544:                 Resource = escalus_client:resource(Alice),
  545:                 Args = [Username, Domain, Resource, "Because I can!"],
  546: 
  547:                 {_, 0} = mongooseimctl("kick_session", Args, Config),
  548:                 Stanza = escalus:wait_for_stanza(Alice),
  549:                 escalus:assert(is_stream_error, [<<"conflict">>, <<"Because I can!">>], Stanza)
  550:         end).
  551: 
  552: status(Config) ->
  553:     escalus:story(Config, [{alice, 1}, {mike, 1}, {bob, 1}], fun(User1, User2, User3) ->
  554:                 PriDomain = escalus_client:server(User1),
  555:                 SecDomain = ct:get_config({hosts, mim, secondary_domain}),
  556:                 AwayPresence = escalus_stanza:presence_show(<<"away">>),
  557:                 escalus_client:send(User2, AwayPresence),
  558: 
  559:                 {"2\n", _} = mongooseimctl("status_num", ["available"], Config),
  560: 
  561:                 {"2\n", _} = mongooseimctl("status_num_host", [PriDomain, "available"], Config),
  562:                 {"0\n", _} = mongooseimctl("status_num_host", [SecDomain, "available"], Config),
  563: 
  564:                 {StatusList, _} = mongooseimctl("status_list", ["available"], Config),
  565:                 match_user_status([User1, User3], StatusList),
  566: 
  567:                 {StatusList2, _} = mongooseimctl("status_list_host",
  568:                                                [PriDomain, "available"], Config),
  569:                 match_user_status([User1, User3], StatusList2),
  570:                 {[], _} = mongooseimctl("status_list_host", [SecDomain, "available"], Config)
  571:         end).
  572: 
  573: sessions_info(Config) ->
  574:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(User1, User2, User3) ->
  575:                 Username1 = escalus_client:username(User1),
  576:                 PriDomain = escalus_client:server(User1),
  577:                 SecDomain = ct:get_config({hosts, mim, secondary_domain}),
  578:                 AwayPresence = escalus_stanza:presence_show(<<"away">>),
  579:                 escalus_client:send(User2, AwayPresence),
  580: 
  581:                 {UserList, _} = mongooseimctl("connected_users_info", [], Config),
  582:                 match_user_info([User1, User2, User3], UserList),
  583: 
  584:                 {UserList2, _} = mongooseimctl("connected_users_vhost", [PriDomain], Config),
  585:                 match_user_info([User1, User2, User3], UserList2),
  586:                 {[], _} = mongooseimctl("connected_users_vhost", [SecDomain], Config),
  587: 
  588:                 {UserList3, _} = mongooseimctl("user_sessions_info",
  589:                                                [Username1, PriDomain], Config),
  590:                 match_user_info([User1], UserList3)
  591:         end).
  592: 
  593: set_presence(Config) ->
  594:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  595:                 Username = escalus_client:username(Alice),
  596:                 Domain = escalus_client:server(Alice),
  597:                 Resource = escalus_client:resource(Alice),
  598: 
  599:                 {_, 0} = mongooseimctl("set_presence",
  600:                                      [Username, Domain, Resource,
  601:                                       "available", "away", "mystatus", "10"],
  602:                                      Config),
  603:                 Presence = escalus:wait_for_stanza(Alice),
  604:                 escalus:assert(is_presence_with_show, [<<"away">>], Presence),
  605:                 escalus:assert(is_presence_with_status, [<<"mystatus">>], Presence),
  606:                 escalus:assert(is_presence_with_priority, [<<"10">>], Presence)
  607:         end).
  608: 
  609: %%--------------------------------------------------------------------
  610: %% mod_admin_extra_vcard tests
  611: %%--------------------------------------------------------------------
  612: 
  613: vcard_rw(Config) ->
  614:     {Username, Domain, _} = get_user_data(alice, Config),
  615: 
  616:     {_, ExitCode} = mongooseimctl("get_vcard", [Username, Domain, "NICKNAME"], Config),
  617:     true = (ExitCode /= 0),
  618: 
  619:     {_, 0} = mongooseimctl("set_vcard", [Username, Domain, "NICKNAME", "SomeNickname"], Config),
  620:     {"SomeNickname\n", 0} = mongooseimctl("get_vcard", [Username, Domain, "NICKNAME"], Config).
  621: 
  622: vcard2_rw(Config) ->
  623:     {Username, Domain, _} = get_user_data(alice, Config),
  624: 
  625:     {_, ExitCode} = mongooseimctl("get_vcard2", [Username, Domain, "ORG", "ORGNAME"], Config),
  626:     true = (ExitCode /= 0),
  627: 
  628:     {_, 0} = mongooseimctl("set_vcard2", [Username, Domain, "ORG", "ORGNAME", "ESL"], Config),
  629:     {"ESL\n", 0} = mongooseimctl("get_vcard2", [Username, Domain, "ORG", "ORGNAME"], Config).
  630: 
  631: vcard2_multi_rw(Config) ->
  632:     {Username, Domain, _} = get_user_data(alice, Config),
  633: 
  634:     {_, ExitCode} = mongooseimctl("get_vcard2_multi", [Username, Domain, "ORG", "ORGUNIT"], Config),
  635:     true = (ExitCode /= 0),
  636: 
  637:     Args = [Username, Domain, "ORG", "ORGUNIT", "sales;marketing"],
  638:     {_, 0} = mongooseimctl("set_vcard2_multi", Args, Config),
  639:     {OrgUnits0, 0} = mongooseimctl("get_vcard2_multi",
  640:                                    [Username, Domain, "ORG", "ORGUNIT"], Config),
  641:     OrgUnits = string:tokens(OrgUnits0, "\n"),
  642:     2 = length(OrgUnits),
  643:     true = (lists:member("sales", OrgUnits) andalso lists:member("marketing", OrgUnits)).
  644: 
  645: %%--------------------------------------------------------------------
  646: %% mod_admin_extra_vcard tests
  647: %%--------------------------------------------------------------------
  648: 
  649: rosteritem_rw(Config) ->
  650:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  651:                 BobJid = escalus_users:get_jid(Config, bob),
  652:                 MikeJid = escalus_users:get_jid(Config, mike),
  653: 
  654:                 {AliceName, Domain, _} = get_user_data(alice, Config),
  655:                 {BobName, Domain, _} = get_user_data(bob, Config),
  656:                 {MikeName, Domain, _} = get_user_data(mike, Config),
  657: 
  658:                 {_, 0} = add_rosteritem1(AliceName, Domain, BobName, Config),
  659:                 {_, 0} = mongooseimctl("add_rosteritem",
  660:                                      [AliceName, Domain, MikeName,
  661:                                       Domain, "My Mike",
  662:                                       "My Group", "both"], Config),
  663: 
  664:                 [Push1, Push2] = escalus:wait_for_stanzas(Alice, 2), % Check roster broadcasts
  665:                 escalus:assert(is_roster_set, Push1),
  666:                 escalus:assert(roster_contains, [BobJid], Push1),
  667:                 escalus:assert(is_roster_set, Push2),
  668:                 escalus:assert(roster_contains, [MikeJid], Push2),
  669: 
  670:                 {Items1, 0} = mongooseimctl("get_roster", [AliceName, Domain], Config),
  671:                 match_roster([{BobName, Domain, "MyBob", "MyGroup", "both"},
  672:                               {MikeName, Domain, "MyMike", "MyGroup", "both"}], Items1),
  673: 
  674:                 escalus:send(Alice, escalus_stanza:roster_get()),
  675:                 Roster1 = escalus:wait_for_stanza(Alice),
  676:                 escalus:assert(is_roster_result, Roster1),
  677:                 escalus:assert(roster_contains, [BobJid], Roster1),
  678:                 escalus:assert(roster_contains, [MikeJid], Roster1),
  679: 
  680:                 {_, 0} = mongooseimctl("delete_rosteritem",
  681:                                      [AliceName, Domain, BobName, Domain],
  682:                                      Config),
  683: 
  684:                 Push3 = escalus:wait_for_stanza(Alice),
  685:                 escalus:assert(is_roster_set, Push3),
  686:                 escalus:assert(roster_contains, [BobJid], Push3),
  687: 
  688:                 {Items2, 0} = mongooseimctl("get_roster", [AliceName, Domain], Config),
  689:                 match_roster([{MikeName, Domain, "MyMike", "MyGroup", "both"}], Items2),
  690: 
  691:                 escalus:send(Alice, escalus_stanza:roster_remove_contact(MikeJid)),  % cleanup
  692:                 escalus:wait_for_stanzas(Alice, 2, 5000)
  693:         end).
  694: 
  695: presence_after_add_rosteritem(Config) ->
  696:      escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  697:                  BobJid = escalus_users:get_jid(Config, bob),
  698:                  {AliceName, Domain, _} = get_user_data(alice, Config),
  699:                  {BobName, Domain, _} = get_user_data(bob, Config),
  700: 
  701:                  {_, 0} = add_rosteritem1(AliceName, Domain, BobName, Config),
  702: 
  703:                  escalus:send(Alice, escalus_stanza:presence(<<"available">>)),
  704:                  escalus:assert(is_presence, escalus:wait_for_stanza(Bob)),
  705: 
  706:                  escalus:send(Alice, escalus_stanza:roster_remove_contact(BobJid)),  % cleanup
  707:                  %% Wait for stanzas, so they would not end up in the next story
  708:                  escalus:wait_for_stanzas(Alice, 3, 5000)
  709:          end).
  710: 
  711: push_roster(Config) ->
  712:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  713:                 BobJid = escalus_users:get_jid(Config, bob),
  714:                 {AliceName, Domain, _} = get_user_data(alice, Config),
  715:                 TemplatePath = escalus_config:get_config(roster_template, Config),
  716: 
  717:                 {_, 0} = mongooseimctl("push_roster", [TemplatePath, AliceName, Domain], Config),
  718:                 escalus:send(Alice, escalus_stanza:roster_get()),
  719:                 Roster1 = escalus:wait_for_stanza(Alice),
  720:                 escalus:assert(is_roster_result, Roster1),
  721:                 escalus:assert(roster_contains, [BobJid], Roster1),
  722: 
  723:                 escalus:send(Alice, escalus_stanza:roster_remove_contact(BobJid)), % cleanup
  724:                 escalus:wait_for_stanzas(Alice, 2, 5000)
  725:         end).
  726: 
  727: process_rosteritems_list_simple(Config) ->
  728:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  729:         %% given
  730:         Action = "list",
  731:         Subs = "any",
  732:         Asks = "any",
  733:         User = escalus_client:short_jid(Alice),
  734:         Contact = string:to_lower(binary_to_list(escalus_client:short_jid(Bob))),
  735:         {AliceName, Domain, _} = get_user_data(alice, Config),
  736:         {BobName, Domain, _} = get_user_data(bob, Config),
  737:         %% when
  738:         {_, 0} = add_rosteritem1(AliceName, Domain, BobName, Config),
  739:         _S = escalus:wait_for_stanzas(Alice, 2),
  740:         {R, 0} = mongooseimctl("process_rosteritems", [Action, Subs, Asks, User, Contact], Config),
  741:         %% then
  742:         {match, _} = re:run(R, ".*Matches:.*" ++ Contact ++ ".*"),
  743:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, BobName, Domain], Config)
  744:     end).
  745: 
  746: process_rosteritems_list_nomatch(Config) ->
  747:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  748:         %% given
  749:         Action = "list",
  750:         Subs = "from:both",
  751:         Asks = "any",
  752:         User = escalus_client:short_jid(Alice),
  753:         Contact =string:to_lower(binary_to_list(escalus_client:short_jid(Bob))),
  754:         {AliceName, Domain, _} = get_user_data(alice, Config),
  755:         {BobName, Domain, _} = get_user_data(bob, Config),
  756:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, BobName,
  757:                                                 Domain, "MyBob", "MyGroup", "to"], Config),
  758:         escalus:wait_for_stanzas(Alice, 2),
  759:         %% when
  760:         {R, 0} = mongooseimctl("process_rosteritems", [Action, Subs, Asks, User, Contact], Config),
  761:         %% then
  762:         nomatch = re:run(R, ".*Matches:.*" ++ Contact ++ ".*"),
  763:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, BobName, Domain], Config)
  764:     end).
  765: 
  766: process_rosteritems_list_advanced1(Config) ->
  767:     escalus:story(Config, [{alice, 1}, {mike, 1}, {kate, 1}], fun(Alice, Mike, Kate) ->
  768:         %% given
  769:         Action = "list",
  770:         Subs = "from:both",
  771:         Asks = "any",
  772:         User = escalus_client:short_jid(Alice),
  773:         {AliceName, Domain, _} = get_user_data(alice, Config),
  774:         {MikeName, Domain, _} = get_user_data(mike, Config),
  775:         {KateName, Domain, _} = get_user_data(kate, Config),
  776:         ContactMike = string:to_lower(binary_to_list(escalus_client:short_jid(Mike))),
  777:         ContactKate= string:to_lower(binary_to_list(escalus_client:short_jid(Kate))),
  778:         ContactsRegexp = ContactMike ++ ":" ++
  779:                          string:substr(binary_to_list(KateName), 1, 2) ++
  780:                          ".*@.*",
  781: 
  782:         {_, 0} = add_rosteritem2(AliceName, Domain, MikeName, Domain, Config),
  783:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, KateName,
  784:                                                 Domain, "BestFriend", "MyGroup", "both"], Config),
  785:         escalus:wait_for_stanzas(Alice, 4),
  786:         %% when
  787:         {R, 0} = mongooseimctl("process_rosteritems",
  788:                              [Action, Subs, Asks, User, ContactsRegexp],
  789:                              Config),
  790:         %% then
  791:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactMike ++ ".*"),
  792:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactKate ++ ".*"),
  793:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, MikeName, Domain], Config),
  794:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, KateName, Domain], Config)
  795:     end).
  796: 
  797: process_rosteritems_delete_advanced(Config) ->
  798:     escalus:story(Config, [{alice, 1}, {mike, 1}, {kate, 1}], fun(Alice, Mike, Kate) ->
  799:         %% given
  800:         Action = "delete",
  801:         Subs = "from",
  802:         Asks = "any",
  803:         User = escalus_client:short_jid(Alice),
  804:         {AliceName, Domain, _} = get_user_data(alice, Config),
  805:         {MikeName, Domain, _} = get_user_data(mike, Config),
  806:         {KateName, Domain, _} = get_user_data(kate, Config),
  807:         ContactMike = string:to_lower(binary_to_list(escalus_client:short_jid(Mike))),
  808:         ContactKate= string:to_lower(binary_to_list(escalus_client:short_jid(Kate))),
  809:         ContactsRegexp = ".*" ++ string:substr(ContactMike, 3) ++
  810:                          ":" ++ string:substr(ContactKate, 1, 2) ++
  811:                          "@" ++ binary_to_list(Domain),
  812:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, MikeName,
  813:                                                 Domain, "DearMike", "MyGroup", "from"], Config),
  814:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, KateName,
  815:                                                 Domain, "Friend", "MyGroup", "from"], Config),
  816:         escalus:wait_for_stanzas(Alice, 4),
  817:         %% when
  818:         {R, 0} = mongooseimctl("process_rosteritems",
  819:                              [Action, Subs, Asks, User, ContactsRegexp],
  820:                              Config),
  821:         %% then
  822:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactMike ++ ".*"),
  823:         nomatch = re:run(R, ".*Matches:.*" ++ ContactKate ++ ".*"),
  824:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, MikeName, Domain], Config),
  825:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, KateName, Domain], Config)
  826:     end).
  827: 
  828: process_rosteritems_list_advanced2(Config) ->
  829:     escalus:story(Config, [{alice, 1}, {mike, 1}, {kate, 1}], fun(Alice, Mike, Kate) ->
  830:         %% given
  831:         Action = "list",
  832:         Subs = "any",
  833:         Asks = "any",
  834:         User = escalus_client:short_jid(Alice),
  835:         {AliceName, Domain, _} = get_user_data(alice, Config),
  836:         {MikeName, Domain, _} = get_user_data(mike, Config),
  837:         {KateName, Domain, _} = get_user_data(kate, Config),
  838:         ContactMike = string:to_lower(binary_to_list(escalus_client:short_jid(Mike))),
  839:         ContactKate= string:to_lower(binary_to_list(escalus_client:short_jid(Kate))),
  840:         ContactsRegexp = ".*e@lo.*",
  841:         {_, 0} = add_rosteritem2(AliceName, Domain, MikeName, Domain, Config),
  842:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, KateName,
  843:                                                 Domain, "KateFromSchool",
  844:                                                 "MyGroup", "from"], Config),
  845:         escalus:wait_for_stanzas(Alice, 4),
  846:         %% when
  847:         {R, 0} = mongooseimctl("process_rosteritems",
  848:                              [Action, Subs, Asks, User, ContactsRegexp],
  849:                              Config),
  850:         %% then
  851:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactMike ++ ".*"),
  852:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactKate ++ ".*"),
  853:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, MikeName, Domain], Config),
  854:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, KateName, Domain], Config)
  855:     end).
  856: 
  857: process_rosteritems_delete_advanced2(Config) ->
  858:     escalus:story(Config, [{alice, 1}, {bob, 1}, {mike, 1}, {kate, 1}],
  859:       fun(Alice, Bob, Mike, Kate) ->
  860:         %% given
  861:         Action = "delete",
  862:         Subs = "to:from",
  863:         Asks = "any",
  864:         User = "al.c[e]@.*host:((b[o]b)|(mike))@loc.*t2",
  865:         {AliceName, Domain, _} = get_user_data(alice, Config),
  866:         {BobName, Domain, _} = get_user_data(bob, Config),
  867:         {MikeName, Domain, _} = get_user_data(mike, Config),
  868:         {KateName, Domain, _} = get_user_data(kate, Config),
  869:         ContactMike = string:to_lower(binary_to_list(escalus_client:short_jid(Mike))),
  870:         ContactKate= string:to_lower(binary_to_list(escalus_client:short_jid(Kate))),
  871:         ContactBob= string:to_lower(binary_to_list(escalus_client:short_jid(Bob))),
  872:         ContactsReg = ".ik[ea]@localho+.*:k@loc.*st:(alice)+@.*:no",
  873:         {_, 0} = mongooseimctl("add_rosteritem",
  874:                              [AliceName, Domain, MikeName,
  875:                               Domain, "DearMike", "MyGroup", "to"],
  876:                              Config),
  877:         {_, 0} = mongooseimctl("add_rosteritem",
  878:                              [AliceName, Domain, KateName,
  879:                               Domain, "HateHerSheHasSoNiceLegs",
  880:                               "MyGroup", "to"], Config),
  881:         {_, 0} = mongooseimctl("add_rosteritem", [BobName, Domain, AliceName,
  882:                                                 Domain, "Girlfriend", "MyGroup", "from"], Config),
  883:         escalus:wait_for_stanzas(Alice, 4),
  884:         escalus:wait_for_stanzas(Bob, 2),
  885:         %% when
  886:         {R, 0} = mongooseimctl("process_rosteritems",
  887:                              [Action, Subs, Asks, User, ContactsReg],
  888:                              Config),
  889:         %% then
  890:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactMike ++ ".*"),
  891:         nomatch = re:run(R, ".*Matches:.*" ++ ContactKate ++ ".*"),
  892:         nomatch = re:run(R, ".*Matches:.*" ++ ContactBob ++ ".*"),
  893:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, MikeName, Domain], Config),
  894:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, KateName, Domain], Config),
  895:         {_, 0} = mongooseimctl("delete_rosteritem", [BobName, Domain, AliceName, Domain], Config)
  896:     end).
  897: 
  898: push_roster_all(Config) ->
  899:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  900:                 TemplatePath = escalus_config:get_config(roster_template, Config),
  901: 
  902:                 {_, 0} = mongooseimctl("push_roster_all", [TemplatePath], Config),
  903: 
  904:                 escalus:send(Alice, escalus_stanza:roster_get()),
  905:                 Roster1 = escalus:wait_for_stanza(Alice),
  906:                 escalus:assert(is_roster_result, Roster1),
  907:                 BobJid = escalus_client:short_jid(Bob),
  908:                 escalus:assert(roster_contains, [BobJid], Roster1),
  909: 
  910:                 escalus:send(Bob, escalus_stanza:roster_get()),
  911:                 Roster2 = escalus:wait_for_stanza(Bob),
  912:                 escalus:assert(is_roster_result, Roster2),
  913:                 AliceJid = escalus_client:short_jid(Alice),
  914:                 escalus:assert(roster_contains, [AliceJid], Roster2),
  915: 
  916:                 escalus:send_and_wait(Alice, escalus_stanza:roster_remove_contact(bob)), % cleanup
  917:                 escalus:send_and_wait(Bob, escalus_stanza:roster_remove_contact(alice)) % cleanup
  918:         end).
  919: 
  920: push_roster_alltoall(Config) ->
  921:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  922:                 BobJid = escalus_users:get_jid(Config, bob),
  923:                 MikeJid = escalus_users:get_jid(Config, mike),
  924:                 KateJid = escalus_users:get_jid(Config, kate),
  925:                 {_, Domain, _} = get_user_data(alice, Config),
  926: 
  927:                 {_, 0} = mongooseimctl("push_roster_alltoall", [Domain, "MyGroup"], Config),
  928: 
  929:                 escalus:send(Alice, escalus_stanza:roster_get()),
  930:                 Roster = escalus:wait_for_stanza(Alice),
  931: 
  932:                 escalus:assert(is_roster_result, Roster),
  933:                 escalus:assert(roster_contains, [BobJid], Roster),
  934:                 escalus:assert(roster_contains, [MikeJid], Roster),
  935:                 escalus:assert(roster_contains, [KateJid], Roster)
  936:         end).
  937: 
  938: %%--------------------------------------------------------------------
  939: %% mod_admin_extra_last tests
  940: %%--------------------------------------------------------------------
  941: 
  942: set_last(Config) ->
  943:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  944:                 BobJid = escalus_users:get_jid(Config, bob),
  945:                 {AliceName, Domain, _} = get_user_data(alice, Config),
  946:                 {BobName, Domain, _} = get_user_data(bob, Config),
  947: 
  948:                 {_, 0} = add_rosteritem1(AliceName, Domain, BobName, Config),
  949:                 {_, 0} = mongooseimctl("add_rosteritem",
  950:                                      [BobName, Domain, AliceName,
  951:                                       Domain, "MyAlice", "MyGroup", "both"],
  952:                                      Config),
  953: 
  954:                 escalus:wait_for_stanza(Alice), % ignore push
  955: 
  956:                 Now = os:system_time(second),
  957:                 TS = integer_to_list(Now - 7200),
  958:                 {_, 0} = mongooseimctl("set_last", [BobName, Domain, TS, "Status"], Config),
  959:                 escalus:send(Alice, escalus_stanza:last_activity(BobJid)),
  960:                 LastAct = escalus:wait_for_stanza(Alice),
  961:                 escalus:assert(is_last_result, LastAct),
  962:                 Seconds = list_to_integer(binary_to_list(
  963:                             exml_query:path(LastAct, [{element, <<"query">>},
  964:                             {attr, <<"seconds">>}]))),
  965:                 true = (( (Seconds > 7100) andalso (Seconds < 7300) ) orelse Seconds),
  966: 
  967:                 {_, 0} = mongooseimctl("delete_rosteritem",
  968:                                      [AliceName, Domain, BobName, Domain],
  969:                                      Config), % cleanup
  970:                 {_, 0} = mongooseimctl("delete_rosteritem",
  971:                                      [BobName, Domain, AliceName, Domain],
  972:                                      Config)
  973:         end).
  974: 
  975: %%--------------------------------------------------------------------
  976: %% mod_admin_extra_private tests
  977: %%--------------------------------------------------------------------
  978: 
  979: private_rw(Config) ->
  980:     {AliceName, Domain, _} = get_user_data(alice, Config),
  981:     XmlEl1 = "<secretinfo xmlns=\"nejmspejs\">1</secretinfo>",
  982:     XmlEl2 = "<secretinfo xmlns=\"inny\">2</secretinfo>",
  983: 
  984:     {_, 0} = mongooseimctl("private_set", [AliceName, Domain, XmlEl1], Config),
  985:     {_, 0} = mongooseimctl("private_set", [AliceName, Domain, XmlEl2], Config),
  986: 
  987:     {Result, 0} = mongooseimctl("private_get",
  988:                               [AliceName, Domain, "secretinfo", "nejmspejs"],
  989:                               Config),
  990:     {ok, #xmlel{ name = <<"secretinfo">>, attrs = [{<<"xmlns">>, <<"nejmspejs">>}],
  991:                 children = [#xmlcdata{ content = <<"1">> }]}} = exml:parse(list_to_binary(Result)).
  992: 
  993: %%--------------------------------------------------------------------
  994: %% mod_admin_extra_stanza tests
  995: %%--------------------------------------------------------------------
  996: 
  997: send_message(Config) ->
  998:     escalus:story(Config, [{alice, 1}, {bob, 2}], fun(Alice, Bob1, Bob2) ->
  999:                 {_, 0} = mongooseimctl("send_message_chat", [escalus_client:full_jid(Alice),
 1000:                                                            escalus_client:full_jid(Bob1),
 1001:                                                            "Hi Bob!"], Config),
 1002:                 Stanza1 = escalus:wait_for_stanza(Bob1),
 1003:                 escalus:assert(is_chat_message, [<<"Hi Bob!">>], Stanza1),
 1004: 
 1005:                 {_, 0} = mongooseimctl("send_message_headline",
 1006:                                      [escalus_client:full_jid(Alice),
 1007:                                       escalus_client:short_jid(Bob1),
 1008:                                       "Subj", "Hi Bob!!"], Config),
 1009:                 Stanza2 = escalus:wait_for_stanza(Bob1),
 1010:                 Stanza3 = escalus:wait_for_stanza(Bob2),
 1011:                 escalus:assert(is_headline_message, [<<"Subj">>, <<"Hi Bob!!">>], Stanza2),
 1012:                 escalus:assert(is_headline_message, [<<"Subj">>, <<"Hi Bob!!">>], Stanza3)
 1013:         end).
 1014: 
 1015: send_message_wrong_jid(Config) ->
 1016:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
 1017:         {_, Err1} = mongooseimctl("send_message_chat", ["@@#$%!!.§§£",
 1018:                                                    escalus_client:full_jid(Bob),
 1019:                                                    "Hello bobby!"], Config),
 1020:         {_, Err2} = mongooseimctl("send_message_headline", ["%%@&@&@==//\///",
 1021:                                                        escalus_client:short_jid(Bob),
 1022:                                                        "Subj", "Are
 1023:                                                        you there?"],
 1024:                              Config),
 1025:         true = Err1 =/= 0,
 1026:         true = Err2 =/= 0,
 1027:         escalus_assert:has_no_stanzas(Alice),
 1028:         escalus_assert:has_no_stanzas(Bob)
 1029:     end).
 1030: 
 1031: send_stanza(Config) ->
 1032:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
 1033:                 Domain = escalus_client:server(Alice),
 1034:                 Resource = escalus_client:resource(Alice),
 1035:                 {BobName, _, _} = get_user_data(bob, Config),
 1036:                 BobJID = <<BobName/binary, $@, Domain/binary, $/,
 1037:                            (escalus_client:resource(Bob))/binary>>,
 1038: 
 1039:                 Stanza = Stanza = create_stanza(Alice, BobJID),
 1040:                 {_, 0} = mongooseimctl("send_stanza_c2s",
 1041:                        [BobName, Domain, Resource, Stanza],
 1042:                        Config),
 1043: 
 1044:                 Message = escalus:wait_for_stanza(Alice),
 1045:                 escalus:assert(is_chat_message, [<<"Hi">>], Message),
 1046:                 escalus:assert(is_stanza_from, [Bob], Message)
 1047:         end).
 1048: 
 1049: send_stanzac2s_wrong(Config) ->
 1050:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
 1051:         Domain = escalus_client:server(Alice),
 1052:         Resource = escalus_client:resource(Alice),
 1053:         WrongBobName = "bobby_the_great",
 1054:         {BobName, _, _} = get_user_data(bob, Config),
 1055:         BobJID = <<BobName/binary, $@, Domain/binary, $/, (escalus_client:resource(Bob))/binary>>,
 1056:         Stanza = create_stanza(Alice, BobJID),
 1057:         StanzaWrong = <<"<iq type='get' id='234234'><xmlns='wrongwrong'>">>,
 1058:         {_, Err} = mongooseimctl("send_stanza_c2s",
 1059:                   [WrongBobName, Domain, Resource, Stanza],
 1060:                   Config),
 1061:         {_, Err2} = mongooseimctl("send_stanza_c2s",
 1062:                   [BobName, Domain, Resource,  StanzaWrong],
 1063:                   Config),
 1064: 
 1065:         true = Err =/= 0,
 1066:         true = Err2 =/= 0,
 1067:         escalus_assert:has_no_stanzas(Alice)
 1068:     end).
 1069: 
 1070: create_stanza(Name1, JID2) ->
 1071:     exml:to_binary(escalus_stanza:from(escalus_stanza:chat_to(Name1, "Hi"), JID2)).
 1072: 
 1073: %%--------------------------------------------------------------------
 1074: %% mod_admin_extra_stats tests
 1075: %%--------------------------------------------------------------------
 1076: 
 1077: stats_global(Config) ->
 1078:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(_Alice, _Bob) ->
 1079:                 RegisteredCount = length(escalus_config:get_config(escalus_users, Config, [])),
 1080:                 Registered = integer_to_list(RegisteredCount) ++ "\n",
 1081: 
 1082:                 {UpTime, 0} = mongooseimctl("stats", ["uptimeseconds"], Config),
 1083:                 _ = list_to_integer(string:strip(UpTime, both, $\n)),
 1084:                 {Registered, 0} = mongooseimctl("stats", ["registeredusers"], Config),
 1085: 
 1086:                 {"2\n", 0} = mongooseimctl("stats", ["onlineusersnode"], Config),
 1087: 
 1088:                 {"2\n", 0} = mongooseimctl("stats", ["onlineusers"], Config)
 1089:         end).
 1090: 
 1091: stats_host(Config) ->
 1092:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, _Bob) ->
 1093:                 RegisteredCount = length(escalus_config:get_config(escalus_users, Config, [])),
 1094:                 Registered = integer_to_list(RegisteredCount) ++ "\n",
 1095: 
 1096:                 PriDomain = escalus_client:server(Alice),
 1097:                 SecDomain = ct:get_config({hosts, mim, secondary_domain}),
 1098: 
 1099:                 {Registered, 0} = mongooseimctl("stats_host",
 1100:                                                 ["registeredusers", PriDomain], Config),
 1101:                 {"0\n", 0} = mongooseimctl("stats_host", ["registeredusers", SecDomain], Config),
 1102: 
 1103:                 {"2\n", 0} = mongooseimctl("stats_host", ["onlineusers", PriDomain], Config),
 1104:                 {"0\n", 0} = mongooseimctl("stats_host", ["onlineusers", SecDomain], Config)
 1105:         end).
 1106: 
 1107: %%--------------------------------------------------------------------
 1108: %% mongoose_graphql tests
 1109: %%--------------------------------------------------------------------
 1110: 
 1111: can_execute_admin_queries_with_permissions(Config) ->
 1112:     Query = "query { checkAuth }",
 1113:     Res = mongooseimctl("graphql", [Query], Config),
 1114:     ?assertMatch({_, 0}, Res),
 1115:     Data = element(1, Res),
 1116:     ?assertNotEqual(nomatch, string:find(Data, "AUTHORIZED")).
 1117: 
 1118: can_handle_execution_error(Config) ->
 1119:     Query = "{}",
 1120:     Res = mongooseimctl("graphql", [Query], Config),
 1121:     ?assertMatch({_, 0}, Res),
 1122:     Data = element(1, Res),
 1123:     ?assertNotEqual(nomatch, string:find(Data, "parser_error")).
 1124: 
 1125: graphql_wrong_arguments_number(Config) ->
 1126:     ExpectedFragment = "This command requires",
 1127:     ResNoArgs = mongooseimctl("graphql", [], Config),
 1128:     ?assertMatch({_, 1}, ResNoArgs),
 1129:     Data1 = element(1, ResNoArgs),
 1130:     ?assertNotEqual(nomatch, string:find(Data1, ExpectedFragment)),
 1131: 
 1132:     ResTooManyArgs = mongooseimctl("graphql", ["{}", "{}"], Config),
 1133:     ?assertMatch({_, 1}, ResTooManyArgs),
 1134:     Data2 = element(1, ResTooManyArgs),
 1135:     ?assertNotEqual(nomatch, string:find(Data2, ExpectedFragment)).
 1136: 
 1137: %%-----------------------------------------------------------------
 1138: %% Improve coverage
 1139: %%-----------------------------------------------------------------
 1140: 
 1141: simple_register(Config) ->
 1142:     %% given
 1143:     Domain = domain(),
 1144:     {Name, Password} = {<<"tyler">>, <<"durden">>},
 1145:     %% when
 1146:     {R1, 0} = mongooseimctl("registered_users", [Domain], Config),
 1147:     Before = length(string:tokens(R1, "\n")),
 1148:     {_, 0} = mongooseimctl("register", [Domain, Password], Config),
 1149:     {_, 0} = mongooseimctl("register_identified", [Name, Domain, Password], Config),
 1150: 
 1151:     {R2, 0} = mongooseimctl("registered_users", [Domain], Config),
 1152:     After = length(string:tokens(R2, "\n")),
 1153:     %% then
 1154:     2 = After - Before.
 1155: 
 1156: simple_unregister(Config) ->
 1157:     %% given
 1158:     Domain = domain(),
 1159:     {Name, _} = {<<"tyler">>, <<"durden">>},
 1160:     %% when
 1161:     {_, 0} = mongooseimctl("unregister", [Name, Domain], Config),
 1162:     {R2, 0} = mongooseimctl("registered_users", [Domain], Config),
 1163:     %% then
 1164:     nomatch = re:run(R2, ".*(" ++ binary_to_list(Name) ++ ").*").
 1165: 
 1166: register_twice(Config) ->
 1167:     %% given
 1168:     Domain = domain(),
 1169:     {Name,  Password} = {<<"tyler">>, <<"durden">>},
 1170:     %% when
 1171:     {_, 0} = mongooseimctl("register_identified", [Name, Domain, Password], Config),
 1172:     {R, Code} = mongooseimctl("register_identified", [Name, Domain, Password], Config),
 1173:     %% then
 1174:     {match, _} = re:run(R, ".*(already registered).*"),
 1175:     true = (Code =/= 0),
 1176:     {_, 0} = mongooseimctl("unregister", [Name, Domain], Config).
 1177: 
 1178: backup_restore_mnesia(Config) ->
 1179:     %% given
 1180:     TableName = passwd,
 1181:     TableSize = rpc_call(mnesia, table_info, [TableName, size]),
 1182:     %% Table passwd should not be empty
 1183:     FileName = "backup_mnesia.bup",
 1184:     %% when
 1185:     {R, 0} = mongooseimctl("backup", [FileName], Config),
 1186:     nomatch = re:run(R, ".+"),
 1187:     rpc_call(mnesia, clear_table, [TableName]),
 1188:     0 = rpc_call(mnesia, table_info, [TableName, size]),
 1189:     {R2, 0} = mongooseimctl("restore", [FileName], Config),
 1190:     %% then
 1191:     nomatch = re:run(R2, ".+"),
 1192:     TableSize = rpc_call(mnesia, table_info, [TableName, size]).
 1193: 
 1194: restore_mnesia_wrong(Config) ->
 1195:     FileName = "file that doesnt exist13123.bup",
 1196:     {R2, _} = mongooseimctl("restore", [FileName], Config),
 1197:     {match, Code} = re:run(R2, ".+"),
 1198:     true = (Code =/= 0).
 1199: 
 1200: dump_and_load(Config) ->
 1201:     FileName = "dump.bup",
 1202:     TableName = passwd,
 1203:     %% Table passwd should not be empty
 1204:     TableSize = rpc_call(mnesia, table_info, [TableName, size]),
 1205:     {DumpReturns, 0} = mongooseimctl("dump", [FileName], Config),
 1206:     ct:log("DumpReturns ~p", [DumpReturns]),
 1207:     {ok, DumpData} = rpc_call(file, consult, [FileName]),
 1208:     ct:log("DumpData ~p", [DumpData]),
 1209:     rpc_call(mnesia, clear_table, [TableName]),
 1210:     0 = rpc_call(mnesia, table_info, [TableName, size]),
 1211:     {R, 0} = mongooseimctl("load", [FileName], Config),
 1212:     ct:log("LoadReturns ~p", [R]),
 1213:     {match, _} = re:run(R, ".+"),
 1214:     TableSize = rpc_call(mnesia, table_info, [TableName, size]).
 1215: 
 1216: load_mnesia_wrong(Config) ->
 1217:     FileName = "file that doesnt existRHCP.bup",
 1218:     {R2, Code} = mongooseimctl("restore", [FileName], Config),
 1219:     {match, _} = re:run(R2, ".+"),
 1220:     true = (Code =/= 0).
 1221: 
 1222: dump_table(Config) ->
 1223:     FileName = "dump.mn",
 1224:     TableName = passwd,
 1225:     %% Table passwd should not be empty
 1226:     TableSize = rpc_call(mnesia, table_info, [TableName, size]),
 1227:     {_, 0} = mongooseimctl("dump_table", [FileName, atom_to_list(TableName)], Config),
 1228:     rpc_call(mnesia, clear_table, [TableName]),
 1229:     0 = rpc_call(mnesia, table_info, [TableName, size]),
 1230:     {R, 0} = mongooseimctl("load", [FileName], Config),
 1231:     {match, _} = re:run(R, ".+"),
 1232:     TableSize = rpc_call(mnesia, table_info, [TableName, size]).
 1233: 
 1234: get_loglevel(Config) ->
 1235:     {R, 0} = mongooseimctl("get_loglevel", [], Config),
 1236:     LogLevel = rpc_call(mongoose_logs, get_global_loglevel, []),
 1237:     Regexp = io_lib:format("global loglevel is \(.\)\{1,2\}, which means '~p'", [LogLevel]),
 1238:     {match, _} = re:run(R, Regexp, [{capture, first}]).
 1239: 
 1240: remove_old_messages_test(Config) ->
 1241:     escalus:story(Config, [{alice, 1}], fun(_) ->
 1242:         %% given
 1243:         JidA = nick_to_jid(alice, Config),
 1244:         JidB = nick_to_jid(bob, Config),
 1245:         JidRecordAlice = jid:from_binary(JidA),
 1246:         JidRecordBob = jid:from_binary(JidB),
 1247:         Domain = domain(),
 1248:         Msg1 = escalus_stanza:chat_to(<<"bob@", Domain/binary>>,
 1249:                                       "Hi, how are you? Its old message!"),
 1250:         Msg2 = escalus_stanza:chat_to(<<"bob@", Domain/binary>>,
 1251:                                       "Hello its new message!"),
 1252:         OldTimestamp = fallback_timestamp(10, os:system_time(microsecond)),
 1253:         OfflineOld = generate_offline_message(JidRecordAlice, JidRecordBob, Msg1, OldTimestamp),
 1254:         OfflineNew = generate_offline_message(JidRecordAlice, JidRecordBob, Msg2, os:system_time(microsecond)),
 1255:         {jid, _, _, _, LUser, LServer, _} = JidRecordBob,
 1256:         HostType = host_type(),
 1257:         rpc_call(mod_offline_backend, write_messages, [host_type(), LUser, LServer, [OfflineOld, OfflineNew]]),
 1258:         %% when
 1259:         {_, 0} = mongooseimctl("delete_old_messages", [LServer, "1"], Config),
 1260:         {ok, SecondList} = rpc_call(mod_offline_backend, pop_messages, [HostType, JidRecordBob]),
 1261:         %% then
 1262:         1 = length(SecondList)
 1263:     end).
 1264: 
 1265: remove_expired_messages_test(Config) ->
 1266:     escalus:story(Config, [{mike, 1}], fun(_) ->
 1267:         %% given
 1268:         JidA = nick_to_jid(mike, Config),
 1269:         JidB = nick_to_jid(kate, Config),
 1270:         JidRecordMike = jid:from_binary(JidA),
 1271:         JidRecordKate = jid:from_binary(JidB),
 1272:         Domain = domain(),
 1273:         Msg1 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "Rolling stones"),
 1274:         Msg2 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "Arctic monkeys!"),
 1275:         Msg3 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "More wine..."),
 1276:         Msg4 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "kings of leon"),
 1277:         OldTimestamp = fallback_timestamp(10, os:system_time(microsecond)),
 1278:         ExpirationTime = fallback_timestamp(2, os:system_time(microsecond)),
 1279:         ExpirationTimeFuture= fallback_timestamp(-5, os:system_time(microsecond)),
 1280:         OfflineOld = generate_offline_expired_message(JidRecordMike,
 1281:                                                       JidRecordKate, Msg1,
 1282:                                                       OldTimestamp,
 1283:                                                       ExpirationTime),
 1284:         OfflineNow = generate_offline_expired_message(JidRecordMike,
 1285:                              JidRecordKate, Msg2, os:system_time(microsecond), ExpirationTime),
 1286:         OfflineFuture = generate_offline_expired_message(JidRecordMike,
 1287:                              JidRecordKate, Msg3, os:system_time(microsecond), ExpirationTimeFuture),
 1288:         OfflineFuture2 = generate_offline_expired_message(JidRecordMike,
 1289:                                                           JidRecordKate, Msg4,
 1290:                                                           OldTimestamp,
 1291:                                                           ExpirationTimeFuture),
 1292:         {jid, _, _, _, LUser, LServer, _} = JidRecordKate,
 1293:         Args = [OfflineOld, OfflineNow, OfflineFuture, OfflineFuture2],
 1294:         HostType = host_type(),
 1295:         rpc_call(mod_offline_backend, write_messages, [HostType, LUser, LServer, Args]),
 1296:         %% when
 1297:         {_, 0} = mongooseimctl("delete_expired_messages", [LServer], Config),
 1298:         {ok, SecondList} = rpc_call(mod_offline_backend, pop_messages, [HostType, JidRecordKate]),
 1299:         %% then
 1300:         2 = length(SecondList)
 1301:     end).
 1302: 
 1303: %%-----------------------------------------------------------------
 1304: %% Helpers
 1305: %%-----------------------------------------------------------------
 1306: 
 1307: 
 1308: nick_to_jid(UserName, Config) when is_atom(UserName) ->
 1309:     UserSpec = escalus_users:get_userspec(Config, UserName),
 1310:     escalus_utils:jid_to_lower(escalus_users:get_jid(Config, UserSpec)).
 1311: 
 1312: generate_offline_message(From, To, Msg, TimeStamp) ->
 1313:     {jid, _, _, _, LUser, LServer, _} = To,
 1314:     #offline_msg{us = {LUser, LServer}, timestamp = TimeStamp, expire = never,
 1315:                  from = From, to = To, packet = Msg}.
 1316: 
 1317: generate_offline_expired_message(From, To, Msg, TimeStamp, ExpirationTime) ->
 1318:     {jid, _, _, _, LUser, LServer, _} = To,
 1319:     #offline_msg{us = {LUser, LServer}, timestamp = TimeStamp,
 1320:                  expire = ExpirationTime, from = From, to = To, packet = Msg}.
 1321: 
 1322: 
 1323: fallback_timestamp(HowManyDays, TS_MicroSeconds) ->
 1324:     HowManySeconds = HowManyDays * 86400,
 1325:     HowManyMicroSeconds = erlang:convert_time_unit(HowManySeconds, second, microsecond),
 1326:     TS_MicroSeconds - HowManyMicroSeconds.
 1327: 
 1328: get_user_data(User, Config) when is_atom(User) ->
 1329:     get_user_data(escalus_users:get_options(Config, User, <<"newres">>), Config);
 1330: get_user_data(User, _Config) ->
 1331:     {_, Password} = lists:keyfind(password, 1, User),
 1332:     {_, Username} = lists:keyfind(username, 1, User),
 1333:     {_, Domain} = lists:keyfind(server, 1, User),
 1334:     {Username, Domain, Password}.
 1335: 
 1336: get_md5(AccountPass) ->
 1337:     lists:flatten([io_lib:format("~.16B", [X])
 1338:                    || X <- binary_to_list(crypto:hash(md5, AccountPass))]).
 1339: get_sha(AccountPass) ->
 1340:     lists:flatten([io_lib:format("~.16B", [X])
 1341:                    || X <- binary_to_list(crypto:hash(sha, AccountPass))]).
 1342: 
 1343: set_last(User, Domain, TStamp) ->
 1344:     rpc(mim(), mod_last, store_last_info,
 1345:         [host_type(), escalus_utils:jid_to_lower(User), Domain, TStamp, <<>>]).
 1346: 
 1347: delete_users(_Config) ->
 1348:     lists:foreach(fun({User, Domain}) ->
 1349:                 JID = mongoose_helper:make_jid(User, Domain),
 1350:                 rpc(mim(), ejabberd_auth, remove_user, [JID])
 1351:         end, get_registered_users()).
 1352: 
 1353: %%-----------------------------------------------------------------
 1354: %% Predicates
 1355: %%-----------------------------------------------------------------
 1356: 
 1357: match_user_status(Users, StatusTxt) ->
 1358:     Statuses = string:tokens(StatusTxt, "\n"),
 1359: 
 1360:     true = (length(Users) == length(Statuses)),
 1361:     match_user_status2(Users, Statuses).
 1362: 
 1363: match_user_status2([], _) ->
 1364:     true;
 1365: match_user_status2([User | UserR], Statuses) ->
 1366:     Username = binary_to_list(escalus_client:username(User)),
 1367:     Domain = binary_to_list(escalus_client:server(User)),
 1368:     Resource = binary_to_list(escalus_client:resource(User)),
 1369: 
 1370:     true = lists:any(fun(Status) ->
 1371:                 [Username, Domain, Resource]
 1372:                 =:=
 1373:                 lists:sublist(string:tokens(Status, "\t"), 1, 3)
 1374:         end, Statuses),
 1375:     match_user_status2(UserR, Statuses).
 1376: 
 1377: match_user_info(Users, UsersTxt) ->
 1378:     UsersInfo = string:tokens(UsersTxt, "\n"),
 1379:     case length(Users) == length(UsersInfo) of
 1380:         true ->
 1381:             ok;
 1382:         false ->
 1383:             ct:fail(#{what => match_user_info_failed,
 1384:                       users => Users, user_info => UsersInfo})
 1385:     end,
 1386:     match_user_info2(Users, UsersInfo).
 1387: 
 1388: match_user_info2([], _) ->
 1389:     true;
 1390: match_user_info2([User | UserR], UsersInfo) ->
 1391:     Username = binary_to_list(escalus_client:username(User)),
 1392:     Domain = binary_to_list(escalus_client:server(User)),
 1393:     Resource = binary_to_list(escalus_client:resource(User)),
 1394:     FullJID = Username ++ "@" ++ Domain ++ "/" ++ Resource,
 1395: 
 1396:     true = lists:any(fun(UserInfo) ->
 1397:                 string:str(UserInfo, string:to_lower(FullJID)) =:= 1
 1398:         end, UsersInfo),
 1399:     match_user_info2(UserR, UsersInfo).
 1400: 
 1401: match_roster(ItemsValid, Items) ->
 1402:     ItemsTokens = [ string:tokens(ItemToken, "\t") || ItemToken <- string:tokens(Items, "\n") ],
 1403: 
 1404:     true = (length(ItemsValid) == length(ItemsTokens)),
 1405:     true = lists:all(fun({Username, Domain, _Nick, _Group, _Sub}) ->
 1406:                     JID = escalus_utils:jid_to_lower(<<Username/binary, "@", Domain/binary >>),
 1407:                     lists:any(fun
 1408:                                 ([RosterJID, _Nick, _Sub, "none", _Group]) ->
 1409:                                     JID =:= escalus_utils:jid_to_lower(list_to_binary(RosterJID));
 1410:                                 (_) ->
 1411:                                     false
 1412:                               end, ItemsTokens)
 1413:             end, ItemsValid).
 1414: 
 1415: string_to_binary(List) ->
 1416:     case erlang:system_info(otp_release) of
 1417:         [$R|_] ->
 1418:             list_to_binary(List);
 1419:         _ ->
 1420:             unicode:characters_to_binary(List)
 1421:     end.
 1422: 
 1423: add_rosteritem1(UserName1, Domain, UserName2, Config) ->
 1424:     mongooseimctl("add_rosteritem",
 1425:                 [UserName1, Domain, UserName2,
 1426:                  Domain, "MyBob", "MyGroup", "both"], Config).
 1427: 
 1428: add_rosteritem2(Name1, Domain1, Name2, Domain2, Config) ->
 1429:     mongooseimctl("add_rosteritem",
 1430:                 [Name1, Domain1, Name2,
 1431:                  Domain2, "DearMike", "MyGroup", "both"], Config).