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