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:     MetricName = [backends, auth, check_password],
  450:     OldValue = get_metric(MetricName),
  451:     {_, 0} = mongooseimctl("check_password", [User, Domain, Pass], Config),
  452:     {_, ErrCode} = mongooseimctl("check_password", [User, Domain, <<Pass/binary, "Bad">>], Config),
  453:     mongoose_helper:wait_until(
  454:       fun() -> get_metric(MetricName) end, true,
  455:       #{validator => fun(NewValue) -> OldValue =/= NewValue end, name => ?FUNCTION_NAME}),
  456:     true = (ErrCode =/= 0). %% Must return code other than 0
  457: 
  458: get_metric(MetricName) ->
  459:     HostType = domain_helper:host_type(mim),
  460:     HostTypePrefix = domain_helper:make_metrics_prefix(HostType),
  461:     {ok, Value} = rpc(mim(), mongoose_metrics, get_metric_value, [[HostTypePrefix | MetricName]]),
  462:     Value.
  463: 
  464: check_account(Config) ->
  465:     {User, Domain, _Pass} = get_user_data(alice, Config),
  466: 
  467:     {_, 0} = mongooseimctl("check_account", [User, Domain], Config),
  468:     {_, ErrCode} = mongooseimctl("check_account", [<<User/binary, "Bad">>, Domain], Config),
  469:     true = (ErrCode =/= 0). %% Must return code other than 0
  470: 
  471: ban_account(Config) ->
  472:     {User, Domain, Pass} = get_user_data(mike, Config),
  473: 
  474:     {ok, Mike} = escalus_client:start_for(Config, mike, <<"newres">>),
  475:     {_, 0} = mongooseimctl("ban_account", [User, Domain, "SomeReason"], Config),
  476:     escalus:assert(is_stream_error, [<<"conflict">>, <<"SomeReason">>],
  477:                    escalus:wait_for_stanza(Mike)),
  478:     {error, {connection_step_failed, _, _}} = escalus_client:start_for(Config, mike, <<"newres2">>),
  479:     mongooseimctl("change_password", [User, Domain, Pass], Config),
  480:     escalus_connection:wait_for_close(Mike, 1000),
  481:     escalus_cleaner:remove_client(Config, Mike).
  482: 
  483: num_active_users(Config) ->
  484:     %% Given some users with recorded last activity timestamps
  485:     {AliceName, Domain, _} = get_user_data(alice, Config),
  486:     {MikeName, Domain, _} = get_user_data(mike, Config),
  487:     {Mega, Secs, _} = os:timestamp(),
  488:     Now = Mega * 1000000 + Secs,
  489:     set_last(AliceName, Domain, Now),
  490:     set_last(MikeName, Domain, Now),
  491:     {SLastActiveBefore, _} = mongooseimctl("num_active_users", [Domain, "5"], Config),
  492:     %% When we artificially remove a user's last activity timestamp in the given period
  493:     TenDaysAgo = Now - 864000,
  494:     set_last(MikeName, Domain, TenDaysAgo),
  495:     %% Then we expect that the number of active users in the last 5 days is one less
  496:     %% than before the change above
  497:     {SLastActiveAfter, _} = mongooseimctl("num_active_users", [Domain, "5"], Config),
  498:     NLastActiveBefore = list_to_integer(string:strip(SLastActiveBefore, both, $\n)),
  499:     NLastActiveAfter = list_to_integer(string:strip(SLastActiveAfter, both, $\n)),
  500:     NLastActiveAfter = NLastActiveBefore - 1.
  501: 
  502: delete_old_users(Config) ->
  503:     {AliceName, Domain, _} = get_user_data(alice, Config),
  504:     {BobName, Domain, _} = get_user_data(bob, Config),
  505:     {KateName, Domain, _} = get_user_data(kate, Config),
  506:     {MikeName, Domain, _} = get_user_data(mike, Config),
  507: 
  508:     {Mega, Secs, _} = os:timestamp(),
  509:     Now = Mega*1000000+Secs,
  510:     set_last(AliceName, Domain, Now),
  511:     set_last(BobName, Domain, Now),
  512:     set_last(MikeName, Domain, Now),
  513:     set_last(KateName, Domain, 0),
  514: 
  515:     {_, 0} = mongooseimctl("delete_old_users", ["10"], Config),
  516:     {_, 0} = mongooseimctl("check_account", [AliceName, Domain], Config),
  517:     {_, ErrCode} = mongooseimctl("check_account", [KateName, Domain], Config),
  518:     true = (ErrCode =/= 0). %% Must return code other than 0
  519: 
  520: delete_old_users_vhost(Config) ->
  521:     {AliceName, Domain, _} = get_user_data(alice, Config),
  522:     {KateName, Domain, KatePass} = get_user_data(kate, Config),
  523:     SecDomain = ct:get_config({hosts, mim, secondary_domain}),
  524: 
  525:     {Mega, Secs, _} = os:timestamp(),
  526:     Now = Mega*1000000+Secs,
  527:     set_last(AliceName, Domain, Now-86400*30),
  528: 
  529:     {_, 0} = mongooseimctl("register_identified", [KateName, SecDomain, KatePass], Config),
  530:     {_, 0} = mongooseimctl("check_account", [KateName, SecDomain], Config),
  531:     {_, 0} = mongooseimctl("delete_old_users_vhost", [SecDomain, "10"], Config),
  532:     {_, 0} = mongooseimctl("check_account", [AliceName, Domain], Config),
  533:     {_, ErrCode} = mongooseimctl("check_account", [KateName, SecDomain], Config),
  534:     true = (ErrCode =/= 0). %% Must return code other than 0
  535: 
  536: %%--------------------------------------------------------------------
  537: %% mod_admin_extra_accounts tests
  538: %%--------------------------------------------------------------------
  539: 
  540: %% Checks both num_resources and resource_num
  541: num_resources_num(Config) ->
  542:     escalus:story(Config, [{alice, 3}, {bob, 1}], fun(_, Alice2, _, _) ->
  543:                 {Username, Domain, _} = get_user_data(alice, Config),
  544:                 ResName = binary_to_list(escalus_client:resource(Alice2)) ++ "\n",
  545: 
  546:                 {"3\n", _} = mongooseimctl("num_resources", [Username, Domain], Config),
  547:                 {ResName, _} = mongooseimctl("resource_num", [Username, Domain, "2"], Config)
  548:         end).
  549: 
  550: kick_session(Config) ->
  551:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  552:                 Username = escalus_client:username(Alice),
  553:                 Domain = escalus_client:server(Alice),
  554:                 Resource = escalus_client:resource(Alice),
  555:                 Args = [Username, Domain, Resource, "Because I can!"],
  556: 
  557:                 {_, 0} = mongooseimctl("kick_session", Args, Config),
  558:                 Stanza = escalus:wait_for_stanza(Alice),
  559:                 escalus:assert(is_stream_error, [<<"conflict">>, <<"Because I can!">>], Stanza)
  560:         end).
  561: 
  562: status(Config) ->
  563:     escalus:story(Config, [{alice, 1}, {mike, 1}, {bob, 1}], fun(User1, User2, User3) ->
  564:                 PriDomain = escalus_client:server(User1),
  565:                 SecDomain = ct:get_config({hosts, mim, secondary_domain}),
  566:                 AwayPresence = escalus_stanza:presence_show(<<"away">>),
  567:                 escalus_client:send(User2, AwayPresence),
  568: 
  569:                 {"2\n", _} = mongooseimctl("status_num", ["available"], Config),
  570: 
  571:                 {"2\n", _} = mongooseimctl("status_num_host", [PriDomain, "available"], Config),
  572:                 {"0\n", _} = mongooseimctl("status_num_host", [SecDomain, "available"], Config),
  573: 
  574:                 {StatusList, _} = mongooseimctl("status_list", ["available"], Config),
  575:                 match_user_status([User1, User3], StatusList),
  576: 
  577:                 {StatusList2, _} = mongooseimctl("status_list_host",
  578:                                                [PriDomain, "available"], Config),
  579:                 match_user_status([User1, User3], StatusList2),
  580:                 {[], _} = mongooseimctl("status_list_host", [SecDomain, "available"], Config)
  581:         end).
  582: 
  583: sessions_info(Config) ->
  584:     escalus:story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(User1, User2, User3) ->
  585:                 Username1 = escalus_client:username(User1),
  586:                 PriDomain = escalus_client:server(User1),
  587:                 SecDomain = ct:get_config({hosts, mim, secondary_domain}),
  588:                 AwayPresence = escalus_stanza:presence_show(<<"away">>),
  589:                 escalus_client:send(User2, AwayPresence),
  590: 
  591:                 {UserList, _} = mongooseimctl("connected_users_info", [], Config),
  592:                 match_user_info([User1, User2, User3], UserList),
  593: 
  594:                 {UserList2, _} = mongooseimctl("connected_users_vhost", [PriDomain], Config),
  595:                 match_user_info([User1, User2, User3], UserList2),
  596:                 {[], _} = mongooseimctl("connected_users_vhost", [SecDomain], Config),
  597: 
  598:                 {UserList3, _} = mongooseimctl("user_sessions_info",
  599:                                                [Username1, PriDomain], Config),
  600:                 match_user_info([User1], UserList3)
  601:         end).
  602: 
  603: set_presence(Config) ->
  604:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  605:                 Username = escalus_client:username(Alice),
  606:                 Domain = escalus_client:server(Alice),
  607:                 Resource = escalus_client:resource(Alice),
  608: 
  609:                 {_, 0} = mongooseimctl("set_presence",
  610:                                      [Username, Domain, Resource,
  611:                                       "available", "away", "mystatus", "10"],
  612:                                      Config),
  613:                 Presence = escalus:wait_for_stanza(Alice),
  614:                 escalus:assert(is_presence_with_show, [<<"away">>], Presence),
  615:                 escalus:assert(is_presence_with_status, [<<"mystatus">>], Presence),
  616:                 escalus:assert(is_presence_with_priority, [<<"10">>], Presence)
  617:         end).
  618: 
  619: %%--------------------------------------------------------------------
  620: %% mod_admin_extra_vcard tests
  621: %%--------------------------------------------------------------------
  622: 
  623: vcard_rw(Config) ->
  624:     {Username, Domain, _} = get_user_data(alice, Config),
  625: 
  626:     {_, ExitCode} = mongooseimctl("get_vcard", [Username, Domain, "NICKNAME"], Config),
  627:     true = (ExitCode /= 0),
  628: 
  629:     {_, 0} = mongooseimctl("set_vcard", [Username, Domain, "NICKNAME", "SomeNickname"], Config),
  630:     {"SomeNickname\n", 0} = mongooseimctl("get_vcard", [Username, Domain, "NICKNAME"], Config).
  631: 
  632: vcard2_rw(Config) ->
  633:     {Username, Domain, _} = get_user_data(alice, Config),
  634: 
  635:     {_, ExitCode} = mongooseimctl("get_vcard2", [Username, Domain, "ORG", "ORGNAME"], Config),
  636:     true = (ExitCode /= 0),
  637: 
  638:     {_, 0} = mongooseimctl("set_vcard2", [Username, Domain, "ORG", "ORGNAME", "ESL"], Config),
  639:     {"ESL\n", 0} = mongooseimctl("get_vcard2", [Username, Domain, "ORG", "ORGNAME"], Config).
  640: 
  641: vcard2_multi_rw(Config) ->
  642:     {Username, Domain, _} = get_user_data(alice, Config),
  643: 
  644:     {_, ExitCode} = mongooseimctl("get_vcard2_multi", [Username, Domain, "ORG", "ORGUNIT"], Config),
  645:     true = (ExitCode /= 0),
  646: 
  647:     Args = [Username, Domain, "ORG", "ORGUNIT", "sales;marketing"],
  648:     {_, 0} = mongooseimctl("set_vcard2_multi", Args, Config),
  649:     {OrgUnits0, 0} = mongooseimctl("get_vcard2_multi",
  650:                                    [Username, Domain, "ORG", "ORGUNIT"], Config),
  651:     OrgUnits = string:tokens(OrgUnits0, "\n"),
  652:     2 = length(OrgUnits),
  653:     true = (lists:member("sales", OrgUnits) andalso lists:member("marketing", OrgUnits)).
  654: 
  655: %%--------------------------------------------------------------------
  656: %% mod_admin_extra_vcard tests
  657: %%--------------------------------------------------------------------
  658: 
  659: rosteritem_rw(Config) ->
  660:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  661:                 BobJid = escalus_users:get_jid(Config, bob),
  662:                 MikeJid = escalus_users:get_jid(Config, mike),
  663: 
  664:                 {AliceName, Domain, _} = get_user_data(alice, Config),
  665:                 {BobName, Domain, _} = get_user_data(bob, Config),
  666:                 {MikeName, Domain, _} = get_user_data(mike, Config),
  667: 
  668:                 {_, 0} = add_rosteritem1(AliceName, Domain, BobName, Config),
  669:                 {_, 0} = mongooseimctl("add_rosteritem",
  670:                                      [AliceName, Domain, MikeName,
  671:                                       Domain, "My Mike",
  672:                                       "My Group", "both"], Config),
  673: 
  674:                 [Push1, Push2] = escalus:wait_for_stanzas(Alice, 2), % Check roster broadcasts
  675:                 escalus:assert(is_roster_set, Push1),
  676:                 escalus:assert(roster_contains, [BobJid], Push1),
  677:                 escalus:assert(is_roster_set, Push2),
  678:                 escalus:assert(roster_contains, [MikeJid], Push2),
  679: 
  680:                 {Items1, 0} = mongooseimctl("get_roster", [AliceName, Domain], Config),
  681:                 match_roster([{BobName, Domain, "MyBob", "MyGroup", "both"},
  682:                               {MikeName, Domain, "MyMike", "MyGroup", "both"}], Items1),
  683: 
  684:                 escalus:send(Alice, escalus_stanza:roster_get()),
  685:                 Roster1 = escalus:wait_for_stanza(Alice),
  686:                 escalus:assert(is_roster_result, Roster1),
  687:                 escalus:assert(roster_contains, [BobJid], Roster1),
  688:                 escalus:assert(roster_contains, [MikeJid], Roster1),
  689: 
  690:                 {_, 0} = mongooseimctl("delete_rosteritem",
  691:                                      [AliceName, Domain, BobName, Domain],
  692:                                      Config),
  693: 
  694:                 Push3 = escalus:wait_for_stanza(Alice),
  695:                 escalus:assert(is_roster_set, Push3),
  696:                 escalus:assert(roster_contains, [BobJid], Push3),
  697: 
  698:                 {Items2, 0} = mongooseimctl("get_roster", [AliceName, Domain], Config),
  699:                 match_roster([{MikeName, Domain, "MyMike", "MyGroup", "both"}], Items2),
  700: 
  701:                 escalus:send(Alice, escalus_stanza:roster_remove_contact(MikeJid)),  % cleanup
  702:                 escalus:wait_for_stanzas(Alice, 2, 5000)
  703:         end).
  704: 
  705: presence_after_add_rosteritem(Config) ->
  706:      escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  707:                  BobJid = escalus_users:get_jid(Config, bob),
  708:                  {AliceName, Domain, _} = get_user_data(alice, Config),
  709:                  {BobName, Domain, _} = get_user_data(bob, Config),
  710: 
  711:                  {_, 0} = add_rosteritem1(AliceName, Domain, BobName, Config),
  712: 
  713:                  escalus:send(Alice, escalus_stanza:presence(<<"available">>)),
  714:                  escalus:assert(is_presence, escalus:wait_for_stanza(Bob)),
  715: 
  716:                  escalus:send(Alice, escalus_stanza:roster_remove_contact(BobJid)),  % cleanup
  717:                  %% Wait for stanzas, so they would not end up in the next story
  718:                  escalus:wait_for_stanzas(Alice, 3, 5000)
  719:          end).
  720: 
  721: push_roster(Config) ->
  722:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  723:                 BobJid = escalus_users:get_jid(Config, bob),
  724:                 {AliceName, Domain, _} = get_user_data(alice, Config),
  725:                 TemplatePath = escalus_config:get_config(roster_template, Config),
  726: 
  727:                 {_, 0} = mongooseimctl("push_roster", [TemplatePath, AliceName, Domain], Config),
  728:                 escalus:send(Alice, escalus_stanza:roster_get()),
  729:                 Roster1 = escalus:wait_for_stanza(Alice),
  730:                 escalus:assert(is_roster_result, Roster1),
  731:                 escalus:assert(roster_contains, [BobJid], Roster1),
  732: 
  733:                 escalus:send(Alice, escalus_stanza:roster_remove_contact(BobJid)), % cleanup
  734:                 escalus:wait_for_stanzas(Alice, 2, 5000)
  735:         end).
  736: 
  737: process_rosteritems_list_simple(Config) ->
  738:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  739:         %% given
  740:         Action = "list",
  741:         Subs = "any",
  742:         Asks = "any",
  743:         User = escalus_client:short_jid(Alice),
  744:         Contact = string:to_lower(binary_to_list(escalus_client:short_jid(Bob))),
  745:         {AliceName, Domain, _} = get_user_data(alice, Config),
  746:         {BobName, Domain, _} = get_user_data(bob, Config),
  747:         %% when
  748:         {_, 0} = add_rosteritem1(AliceName, Domain, BobName, Config),
  749:         _S = escalus:wait_for_stanzas(Alice, 2),
  750:         {R, 0} = mongooseimctl("process_rosteritems", [Action, Subs, Asks, User, Contact], Config),
  751:         %% then
  752:         {match, _} = re:run(R, ".*Matches:.*" ++ Contact ++ ".*"),
  753:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, BobName, Domain], Config)
  754:     end).
  755: 
  756: process_rosteritems_list_nomatch(Config) ->
  757:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  758:         %% given
  759:         Action = "list",
  760:         Subs = "from:both",
  761:         Asks = "any",
  762:         User = escalus_client:short_jid(Alice),
  763:         Contact =string:to_lower(binary_to_list(escalus_client:short_jid(Bob))),
  764:         {AliceName, Domain, _} = get_user_data(alice, Config),
  765:         {BobName, Domain, _} = get_user_data(bob, Config),
  766:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, BobName,
  767:                                                 Domain, "MyBob", "MyGroup", "to"], Config),
  768:         escalus:wait_for_stanzas(Alice, 2),
  769:         %% when
  770:         {R, 0} = mongooseimctl("process_rosteritems", [Action, Subs, Asks, User, Contact], Config),
  771:         %% then
  772:         nomatch = re:run(R, ".*Matches:.*" ++ Contact ++ ".*"),
  773:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, BobName, Domain], Config)
  774:     end).
  775: 
  776: process_rosteritems_list_advanced1(Config) ->
  777:     escalus:story(Config, [{alice, 1}, {mike, 1}, {kate, 1}], fun(Alice, Mike, Kate) ->
  778:         %% given
  779:         Action = "list",
  780:         Subs = "from:both",
  781:         Asks = "any",
  782:         User = escalus_client:short_jid(Alice),
  783:         {AliceName, Domain, _} = get_user_data(alice, Config),
  784:         {MikeName, Domain, _} = get_user_data(mike, Config),
  785:         {KateName, Domain, _} = get_user_data(kate, Config),
  786:         ContactMike = string:to_lower(binary_to_list(escalus_client:short_jid(Mike))),
  787:         ContactKate= string:to_lower(binary_to_list(escalus_client:short_jid(Kate))),
  788:         ContactsRegexp = ContactMike ++ ":" ++
  789:                          string:substr(binary_to_list(KateName), 1, 2) ++
  790:                          ".*@.*",
  791: 
  792:         {_, 0} = add_rosteritem2(AliceName, Domain, MikeName, Domain, Config),
  793:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, KateName,
  794:                                                 Domain, "BestFriend", "MyGroup", "both"], Config),
  795:         escalus:wait_for_stanzas(Alice, 4),
  796:         %% when
  797:         {R, 0} = mongooseimctl("process_rosteritems",
  798:                              [Action, Subs, Asks, User, ContactsRegexp],
  799:                              Config),
  800:         %% then
  801:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactMike ++ ".*"),
  802:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactKate ++ ".*"),
  803:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, MikeName, Domain], Config),
  804:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, KateName, Domain], Config)
  805:     end).
  806: 
  807: process_rosteritems_delete_advanced(Config) ->
  808:     escalus:story(Config, [{alice, 1}, {mike, 1}, {kate, 1}], fun(Alice, Mike, Kate) ->
  809:         %% given
  810:         Action = "delete",
  811:         Subs = "from",
  812:         Asks = "any",
  813:         User = escalus_client:short_jid(Alice),
  814:         {AliceName, Domain, _} = get_user_data(alice, Config),
  815:         {MikeName, Domain, _} = get_user_data(mike, Config),
  816:         {KateName, Domain, _} = get_user_data(kate, Config),
  817:         ContactMike = string:to_lower(binary_to_list(escalus_client:short_jid(Mike))),
  818:         ContactKate= string:to_lower(binary_to_list(escalus_client:short_jid(Kate))),
  819:         ContactsRegexp = ".*" ++ string:substr(ContactMike, 3) ++
  820:                          ":" ++ string:substr(ContactKate, 1, 2) ++
  821:                          "@" ++ binary_to_list(Domain),
  822:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, MikeName,
  823:                                                 Domain, "DearMike", "MyGroup", "from"], Config),
  824:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, KateName,
  825:                                                 Domain, "Friend", "MyGroup", "from"], Config),
  826:         escalus:wait_for_stanzas(Alice, 4),
  827:         %% when
  828:         {R, 0} = mongooseimctl("process_rosteritems",
  829:                              [Action, Subs, Asks, User, ContactsRegexp],
  830:                              Config),
  831:         %% then
  832:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactMike ++ ".*"),
  833:         nomatch = re:run(R, ".*Matches:.*" ++ ContactKate ++ ".*"),
  834:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, MikeName, Domain], Config),
  835:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, KateName, Domain], Config)
  836:     end).
  837: 
  838: process_rosteritems_list_advanced2(Config) ->
  839:     escalus:story(Config, [{alice, 1}, {mike, 1}, {kate, 1}], fun(Alice, Mike, Kate) ->
  840:         %% given
  841:         Action = "list",
  842:         Subs = "any",
  843:         Asks = "any",
  844:         User = escalus_client:short_jid(Alice),
  845:         {AliceName, Domain, _} = get_user_data(alice, Config),
  846:         {MikeName, Domain, _} = get_user_data(mike, Config),
  847:         {KateName, Domain, _} = get_user_data(kate, Config),
  848:         ContactMike = string:to_lower(binary_to_list(escalus_client:short_jid(Mike))),
  849:         ContactKate= string:to_lower(binary_to_list(escalus_client:short_jid(Kate))),
  850:         ContactsRegexp = ".*e@lo.*",
  851:         {_, 0} = add_rosteritem2(AliceName, Domain, MikeName, Domain, Config),
  852:         {_, 0} = mongooseimctl("add_rosteritem", [AliceName, Domain, KateName,
  853:                                                 Domain, "KateFromSchool",
  854:                                                 "MyGroup", "from"], Config),
  855:         escalus:wait_for_stanzas(Alice, 4),
  856:         %% when
  857:         {R, 0} = mongooseimctl("process_rosteritems",
  858:                              [Action, Subs, Asks, User, ContactsRegexp],
  859:                              Config),
  860:         %% then
  861:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactMike ++ ".*"),
  862:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactKate ++ ".*"),
  863:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, MikeName, Domain], Config),
  864:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, KateName, Domain], Config)
  865:     end).
  866: 
  867: process_rosteritems_delete_advanced2(Config) ->
  868:     escalus:story(Config, [{alice, 1}, {bob, 1}, {mike, 1}, {kate, 1}],
  869:       fun(Alice, Bob, Mike, Kate) ->
  870:         %% given
  871:         Action = "delete",
  872:         Subs = "to:from",
  873:         Asks = "any",
  874:         User = "al.c[e]@.*host:((b[o]b)|(mike))@loc.*t2",
  875:         {AliceName, Domain, _} = get_user_data(alice, Config),
  876:         {BobName, Domain, _} = get_user_data(bob, Config),
  877:         {MikeName, Domain, _} = get_user_data(mike, Config),
  878:         {KateName, Domain, _} = get_user_data(kate, Config),
  879:         ContactMike = string:to_lower(binary_to_list(escalus_client:short_jid(Mike))),
  880:         ContactKate= string:to_lower(binary_to_list(escalus_client:short_jid(Kate))),
  881:         ContactBob= string:to_lower(binary_to_list(escalus_client:short_jid(Bob))),
  882:         ContactsReg = ".ik[ea]@localho+.*:k@loc.*st:(alice)+@.*:no",
  883:         {_, 0} = mongooseimctl("add_rosteritem",
  884:                              [AliceName, Domain, MikeName,
  885:                               Domain, "DearMike", "MyGroup", "to"],
  886:                              Config),
  887:         {_, 0} = mongooseimctl("add_rosteritem",
  888:                              [AliceName, Domain, KateName,
  889:                               Domain, "HateHerSheHasSoNiceLegs",
  890:                               "MyGroup", "to"], Config),
  891:         {_, 0} = mongooseimctl("add_rosteritem", [BobName, Domain, AliceName,
  892:                                                 Domain, "Girlfriend", "MyGroup", "from"], Config),
  893:         escalus:wait_for_stanzas(Alice, 4),
  894:         escalus:wait_for_stanzas(Bob, 2),
  895:         %% when
  896:         {R, 0} = mongooseimctl("process_rosteritems",
  897:                              [Action, Subs, Asks, User, ContactsReg],
  898:                              Config),
  899:         %% then
  900:         {match, _} = re:run(R, ".*Matches:.*" ++ ContactMike ++ ".*"),
  901:         nomatch = re:run(R, ".*Matches:.*" ++ ContactKate ++ ".*"),
  902:         nomatch = re:run(R, ".*Matches:.*" ++ ContactBob ++ ".*"),
  903:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, MikeName, Domain], Config),
  904:         {_, 0} = mongooseimctl("delete_rosteritem", [AliceName, Domain, KateName, Domain], Config),
  905:         {_, 0} = mongooseimctl("delete_rosteritem", [BobName, Domain, AliceName, Domain], Config)
  906:     end).
  907: 
  908: push_roster_all(Config) ->
  909:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  910:                 TemplatePath = escalus_config:get_config(roster_template, Config),
  911: 
  912:                 {_, 0} = mongooseimctl("push_roster_all", [TemplatePath], Config),
  913: 
  914:                 escalus:send(Alice, escalus_stanza:roster_get()),
  915:                 Roster1 = escalus:wait_for_stanza(Alice),
  916:                 escalus:assert(is_roster_result, Roster1),
  917:                 BobJid = escalus_client:short_jid(Bob),
  918:                 escalus:assert(roster_contains, [BobJid], Roster1),
  919: 
  920:                 escalus:send(Bob, escalus_stanza:roster_get()),
  921:                 Roster2 = escalus:wait_for_stanza(Bob),
  922:                 escalus:assert(is_roster_result, Roster2),
  923:                 AliceJid = escalus_client:short_jid(Alice),
  924:                 escalus:assert(roster_contains, [AliceJid], Roster2),
  925: 
  926:                 escalus:send_and_wait(Alice, escalus_stanza:roster_remove_contact(bob)), % cleanup
  927:                 escalus:send_and_wait(Bob, escalus_stanza:roster_remove_contact(alice)) % cleanup
  928:         end).
  929: 
  930: push_roster_alltoall(Config) ->
  931:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  932:                 BobJid = escalus_users:get_jid(Config, bob),
  933:                 MikeJid = escalus_users:get_jid(Config, mike),
  934:                 KateJid = escalus_users:get_jid(Config, kate),
  935:                 {_, Domain, _} = get_user_data(alice, Config),
  936: 
  937:                 {_, 0} = mongooseimctl("push_roster_alltoall", [Domain, "MyGroup"], Config),
  938: 
  939:                 escalus:send(Alice, escalus_stanza:roster_get()),
  940:                 Roster = escalus:wait_for_stanza(Alice),
  941: 
  942:                 escalus:assert(is_roster_result, Roster),
  943:                 escalus:assert(roster_contains, [BobJid], Roster),
  944:                 escalus:assert(roster_contains, [MikeJid], Roster),
  945:                 escalus:assert(roster_contains, [KateJid], Roster)
  946:         end).
  947: 
  948: %%--------------------------------------------------------------------
  949: %% mod_admin_extra_last tests
  950: %%--------------------------------------------------------------------
  951: 
  952: set_last(Config) ->
  953:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  954:                 BobJid = escalus_users:get_jid(Config, bob),
  955:                 {AliceName, Domain, _} = get_user_data(alice, Config),
  956:                 {BobName, Domain, _} = get_user_data(bob, Config),
  957: 
  958:                 {_, 0} = add_rosteritem1(AliceName, Domain, BobName, Config),
  959:                 {_, 0} = mongooseimctl("add_rosteritem",
  960:                                      [BobName, Domain, AliceName,
  961:                                       Domain, "MyAlice", "MyGroup", "both"],
  962:                                      Config),
  963: 
  964:                 escalus:wait_for_stanza(Alice), % ignore push
  965: 
  966:                 Now = os:system_time(second),
  967:                 TS = integer_to_list(Now - 7200),
  968:                 {_, 0} = mongooseimctl("set_last", [BobName, Domain, TS, "Status"], Config),
  969:                 escalus:send(Alice, escalus_stanza:last_activity(BobJid)),
  970:                 LastAct = escalus:wait_for_stanza(Alice),
  971:                 escalus:assert(is_last_result, LastAct),
  972:                 Seconds = list_to_integer(binary_to_list(
  973:                             exml_query:path(LastAct, [{element, <<"query">>},
  974:                             {attr, <<"seconds">>}]))),
  975:                 true = (( (Seconds > 7100) andalso (Seconds < 7300) ) orelse Seconds),
  976: 
  977:                 {_, 0} = mongooseimctl("delete_rosteritem",
  978:                                      [AliceName, Domain, BobName, Domain],
  979:                                      Config), % cleanup
  980:                 {_, 0} = mongooseimctl("delete_rosteritem",
  981:                                      [BobName, Domain, AliceName, Domain],
  982:                                      Config)
  983:         end).
  984: 
  985: %%--------------------------------------------------------------------
  986: %% mod_admin_extra_private tests
  987: %%--------------------------------------------------------------------
  988: 
  989: private_rw(Config) ->
  990:     {AliceName, Domain, _} = get_user_data(alice, Config),
  991:     XmlEl1 = "<secretinfo xmlns=\"nejmspejs\">1</secretinfo>",
  992:     XmlEl2 = "<secretinfo xmlns=\"inny\">2</secretinfo>",
  993: 
  994:     {_, 0} = mongooseimctl("private_set", [AliceName, Domain, XmlEl1], Config),
  995:     {_, 0} = mongooseimctl("private_set", [AliceName, Domain, XmlEl2], Config),
  996: 
  997:     {Result, 0} = mongooseimctl("private_get",
  998:                               [AliceName, Domain, "secretinfo", "nejmspejs"],
  999:                               Config),
 1000:     {ok, #xmlel{ name = <<"secretinfo">>, attrs = [{<<"xmlns">>, <<"nejmspejs">>}],
 1001:                 children = [#xmlcdata{ content = <<"1">> }]}} = exml:parse(list_to_binary(Result)).
 1002: 
 1003: %%--------------------------------------------------------------------
 1004: %% mod_admin_extra_stanza tests
 1005: %%--------------------------------------------------------------------
 1006: 
 1007: send_message(Config) ->
 1008:     escalus:story(Config, [{alice, 1}, {bob, 2}], fun(Alice, Bob1, Bob2) ->
 1009:                 {_, 0} = mongooseimctl("send_message_chat", [escalus_client:full_jid(Alice),
 1010:                                                            escalus_client:full_jid(Bob1),
 1011:                                                            "Hi Bob!"], Config),
 1012:                 Stanza1 = escalus:wait_for_stanza(Bob1),
 1013:                 escalus:assert(is_chat_message, [<<"Hi Bob!">>], Stanza1),
 1014: 
 1015:                 {_, 0} = mongooseimctl("send_message_headline",
 1016:                                      [escalus_client:full_jid(Alice),
 1017:                                       escalus_client:short_jid(Bob1),
 1018:                                       "Subj", "Hi Bob!!"], Config),
 1019:                 Stanza2 = escalus:wait_for_stanza(Bob1),
 1020:                 Stanza3 = escalus:wait_for_stanza(Bob2),
 1021:                 escalus:assert(is_headline_message, [<<"Subj">>, <<"Hi Bob!!">>], Stanza2),
 1022:                 escalus:assert(is_headline_message, [<<"Subj">>, <<"Hi Bob!!">>], Stanza3)
 1023:         end).
 1024: 
 1025: send_message_wrong_jid(Config) ->
 1026:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
 1027:         {_, Err1} = mongooseimctl("send_message_chat", ["@@#$%!!.§§£",
 1028:                                                    escalus_client:full_jid(Bob),
 1029:                                                    "Hello bobby!"], Config),
 1030:         {_, Err2} = mongooseimctl("send_message_headline", ["%%@&@&@==//\///",
 1031:                                                        escalus_client:short_jid(Bob),
 1032:                                                        "Subj", "Are
 1033:                                                        you there?"],
 1034:                              Config),
 1035:         true = Err1 =/= 0,
 1036:         true = Err2 =/= 0,
 1037:         escalus_assert:has_no_stanzas(Alice),
 1038:         escalus_assert:has_no_stanzas(Bob)
 1039:     end).
 1040: 
 1041: send_stanza(Config) ->
 1042:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
 1043:                 Domain = escalus_client:server(Alice),
 1044:                 Resource = escalus_client:resource(Alice),
 1045:                 {BobName, _, _} = get_user_data(bob, Config),
 1046:                 BobJID = <<BobName/binary, $@, Domain/binary, $/,
 1047:                            (escalus_client:resource(Bob))/binary>>,
 1048: 
 1049:                 Stanza = Stanza = create_stanza(Alice, BobJID),
 1050:                 {_, 0} = mongooseimctl("send_stanza_c2s",
 1051:                        [BobName, Domain, Resource, Stanza],
 1052:                        Config),
 1053: 
 1054:                 Message = escalus:wait_for_stanza(Alice),
 1055:                 escalus:assert(is_chat_message, [<<"Hi">>], Message),
 1056:                 escalus:assert(is_stanza_from, [Bob], Message)
 1057:         end).
 1058: 
 1059: send_stanzac2s_wrong(Config) ->
 1060:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
 1061:         Domain = escalus_client:server(Alice),
 1062:         Resource = escalus_client:resource(Alice),
 1063:         WrongBobName = "bobby_the_great",
 1064:         {BobName, _, _} = get_user_data(bob, Config),
 1065:         BobJID = <<BobName/binary, $@, Domain/binary, $/, (escalus_client:resource(Bob))/binary>>,
 1066:         Stanza = create_stanza(Alice, BobJID),
 1067:         StanzaWrong = <<"<iq type='get' id='234234'><xmlns='wrongwrong'>">>,
 1068:         {_, Err} = mongooseimctl("send_stanza_c2s",
 1069:                   [WrongBobName, Domain, Resource, Stanza],
 1070:                   Config),
 1071:         {_, Err2} = mongooseimctl("send_stanza_c2s",
 1072:                   [BobName, Domain, Resource,  StanzaWrong],
 1073:                   Config),
 1074: 
 1075:         true = Err =/= 0,
 1076:         true = Err2 =/= 0,
 1077:         escalus_assert:has_no_stanzas(Alice)
 1078:     end).
 1079: 
 1080: create_stanza(Name1, JID2) ->
 1081:     exml:to_binary(escalus_stanza:from(escalus_stanza:chat_to(Name1, "Hi"), JID2)).
 1082: 
 1083: %%--------------------------------------------------------------------
 1084: %% mod_admin_extra_stats tests
 1085: %%--------------------------------------------------------------------
 1086: 
 1087: stats_global(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:                 {UpTime, 0} = mongooseimctl("stats", ["uptimeseconds"], Config),
 1093:                 _ = list_to_integer(string:strip(UpTime, both, $\n)),
 1094:                 {Registered, 0} = mongooseimctl("stats", ["registeredusers"], Config),
 1095: 
 1096:                 {"2\n", 0} = mongooseimctl("stats", ["onlineusersnode"], Config),
 1097: 
 1098:                 {"2\n", 0} = mongooseimctl("stats", ["onlineusers"], Config)
 1099:         end).
 1100: 
 1101: stats_host(Config) ->
 1102:     escalus:story(Config, [{alice, 1}, {bob, 1}], fun(Alice, _Bob) ->
 1103:                 RegisteredCount = length(escalus_config:get_config(escalus_users, Config, [])),
 1104:                 Registered = integer_to_list(RegisteredCount) ++ "\n",
 1105: 
 1106:                 PriDomain = escalus_client:server(Alice),
 1107:                 SecDomain = ct:get_config({hosts, mim, secondary_domain}),
 1108: 
 1109:                 {Registered, 0} = mongooseimctl("stats_host",
 1110:                                                 ["registeredusers", PriDomain], Config),
 1111:                 {"0\n", 0} = mongooseimctl("stats_host", ["registeredusers", SecDomain], Config),
 1112: 
 1113:                 {"2\n", 0} = mongooseimctl("stats_host", ["onlineusers", PriDomain], Config),
 1114:                 {"0\n", 0} = mongooseimctl("stats_host", ["onlineusers", SecDomain], Config)
 1115:         end).
 1116: 
 1117: %%--------------------------------------------------------------------
 1118: %% mongoose_graphql tests
 1119: %%--------------------------------------------------------------------
 1120: 
 1121: can_execute_admin_queries_with_permissions(Config) ->
 1122:     Query = "query { checkAuth }",
 1123:     Res = mongooseimctl("graphql", [Query], Config),
 1124:     ?assertMatch({_, 0}, Res),
 1125:     Data = element(1, Res),
 1126:     ?assertNotEqual(nomatch, string:find(Data, "AUTHORIZED")).
 1127: 
 1128: can_handle_execution_error(Config) ->
 1129:     Query = "{}",
 1130:     Res = mongooseimctl("graphql", [Query], Config),
 1131:     ?assertMatch({_, 0}, Res),
 1132:     Data = element(1, Res),
 1133:     ?assertNotEqual(nomatch, string:find(Data, "parser_error")).
 1134: 
 1135: graphql_wrong_arguments_number(Config) ->
 1136:     ExpectedFragment = "This command requires",
 1137:     ResNoArgs = mongooseimctl("graphql", [], Config),
 1138:     ?assertMatch({_, 1}, ResNoArgs),
 1139:     Data1 = element(1, ResNoArgs),
 1140:     ?assertNotEqual(nomatch, string:find(Data1, ExpectedFragment)),
 1141: 
 1142:     ResTooManyArgs = mongooseimctl("graphql", ["{}", "{}"], Config),
 1143:     ?assertMatch({_, 1}, ResTooManyArgs),
 1144:     Data2 = element(1, ResTooManyArgs),
 1145:     ?assertNotEqual(nomatch, string:find(Data2, ExpectedFragment)).
 1146: 
 1147: %%-----------------------------------------------------------------
 1148: %% Improve coverage
 1149: %%-----------------------------------------------------------------
 1150: 
 1151: simple_register(Config) ->
 1152:     %% given
 1153:     Domain = domain(),
 1154:     {Name, Password} = {<<"tyler">>, <<"durden">>},
 1155:     %% when
 1156:     {R1, 0} = mongooseimctl("registered_users", [Domain], Config),
 1157:     Before = length(string:tokens(R1, "\n")),
 1158:     {_, 0} = mongooseimctl("register", [Domain, Password], Config),
 1159:     {_, 0} = mongooseimctl("register_identified", [Name, Domain, Password], Config),
 1160: 
 1161:     {R2, 0} = mongooseimctl("registered_users", [Domain], Config),
 1162:     After = length(string:tokens(R2, "\n")),
 1163:     %% then
 1164:     2 = After - Before.
 1165: 
 1166: simple_unregister(Config) ->
 1167:     %% given
 1168:     Domain = domain(),
 1169:     {Name, _} = {<<"tyler">>, <<"durden">>},
 1170:     %% when
 1171:     {_, 0} = mongooseimctl("unregister", [Name, Domain], Config),
 1172:     {R2, 0} = mongooseimctl("registered_users", [Domain], Config),
 1173:     %% then
 1174:     nomatch = re:run(R2, ".*(" ++ binary_to_list(Name) ++ ").*").
 1175: 
 1176: register_twice(Config) ->
 1177:     %% given
 1178:     Domain = domain(),
 1179:     {Name,  Password} = {<<"tyler">>, <<"durden">>},
 1180:     %% when
 1181:     {_, 0} = mongooseimctl("register_identified", [Name, Domain, Password], Config),
 1182:     {R, Code} = mongooseimctl("register_identified", [Name, Domain, Password], Config),
 1183:     %% then
 1184:     {match, _} = re:run(R, ".*(already registered).*"),
 1185:     true = (Code =/= 0),
 1186:     {_, 0} = mongooseimctl("unregister", [Name, Domain], Config).
 1187: 
 1188: backup_restore_mnesia(Config) ->
 1189:     %% given
 1190:     TableName = passwd,
 1191:     TableSize = rpc_call(mnesia, table_info, [TableName, size]),
 1192:     %% Table passwd should not be empty
 1193:     FileName = "backup_mnesia.bup",
 1194:     %% when
 1195:     {R, 0} = mongooseimctl("backup", [FileName], Config),
 1196:     nomatch = re:run(R, ".+"),
 1197:     rpc_call(mnesia, clear_table, [TableName]),
 1198:     0 = rpc_call(mnesia, table_info, [TableName, size]),
 1199:     {R2, 0} = mongooseimctl("restore", [FileName], Config),
 1200:     %% then
 1201:     nomatch = re:run(R2, ".+"),
 1202:     TableSize = rpc_call(mnesia, table_info, [TableName, size]).
 1203: 
 1204: restore_mnesia_wrong(Config) ->
 1205:     FileName = "file that doesnt exist13123.bup",
 1206:     {R2, _} = mongooseimctl("restore", [FileName], Config),
 1207:     {match, Code} = re:run(R2, ".+"),
 1208:     true = (Code =/= 0).
 1209: 
 1210: dump_and_load(Config) ->
 1211:     FileName = "dump.bup",
 1212:     TableName = passwd,
 1213:     %% Table passwd should not be empty
 1214:     TableSize = rpc_call(mnesia, table_info, [TableName, size]),
 1215:     {DumpReturns, 0} = mongooseimctl("dump", [FileName], Config),
 1216:     ct:log("DumpReturns ~p", [DumpReturns]),
 1217:     {ok, DumpData} = rpc_call(file, consult, [FileName]),
 1218:     ct:log("DumpData ~p", [DumpData]),
 1219:     rpc_call(mnesia, clear_table, [TableName]),
 1220:     0 = rpc_call(mnesia, table_info, [TableName, size]),
 1221:     {R, 0} = mongooseimctl("load", [FileName], Config),
 1222:     ct:log("LoadReturns ~p", [R]),
 1223:     {match, _} = re:run(R, ".+"),
 1224:     TableSize = rpc_call(mnesia, table_info, [TableName, size]).
 1225: 
 1226: load_mnesia_wrong(Config) ->
 1227:     FileName = "file that doesnt existRHCP.bup",
 1228:     {R2, Code} = mongooseimctl("restore", [FileName], Config),
 1229:     {match, _} = re:run(R2, ".+"),
 1230:     true = (Code =/= 0).
 1231: 
 1232: dump_table(Config) ->
 1233:     FileName = "dump.mn",
 1234:     TableName = passwd,
 1235:     %% Table passwd should not be empty
 1236:     TableSize = rpc_call(mnesia, table_info, [TableName, size]),
 1237:     {_, 0} = mongooseimctl("dump_table", [FileName, atom_to_list(TableName)], Config),
 1238:     rpc_call(mnesia, clear_table, [TableName]),
 1239:     0 = rpc_call(mnesia, table_info, [TableName, size]),
 1240:     {R, 0} = mongooseimctl("load", [FileName], Config),
 1241:     {match, _} = re:run(R, ".+"),
 1242:     TableSize = rpc_call(mnesia, table_info, [TableName, size]).
 1243: 
 1244: get_loglevel(Config) ->
 1245:     {R, 0} = mongooseimctl("get_loglevel", [], Config),
 1246:     LogLevel = rpc_call(mongoose_logs, get_global_loglevel, []),
 1247:     Regexp = io_lib:format("global loglevel is \(.\)\{1,2\}, which means '~p'", [LogLevel]),
 1248:     {match, _} = re:run(R, Regexp, [{capture, first}]).
 1249: 
 1250: remove_old_messages_test(Config) ->
 1251:     escalus:story(Config, [{alice, 1}], fun(_) ->
 1252:         %% given
 1253:         JidA = nick_to_jid(alice, Config),
 1254:         JidB = nick_to_jid(bob, Config),
 1255:         JidRecordAlice = jid:from_binary(JidA),
 1256:         JidRecordBob = jid:from_binary(JidB),
 1257:         Domain = domain(),
 1258:         Msg1 = escalus_stanza:chat_to(<<"bob@", Domain/binary>>,
 1259:                                       "Hi, how are you? Its old message!"),
 1260:         Msg2 = escalus_stanza:chat_to(<<"bob@", Domain/binary>>,
 1261:                                       "Hello its new message!"),
 1262:         OldTimestamp = fallback_timestamp(10, os:system_time(microsecond)),
 1263:         OfflineOld = generate_offline_message(JidRecordAlice, JidRecordBob, Msg1, OldTimestamp),
 1264:         OfflineNew = generate_offline_message(JidRecordAlice, JidRecordBob, Msg2, os:system_time(microsecond)),
 1265:         {jid, _, _, _, LUser, LServer, _} = JidRecordBob,
 1266:         HostType = host_type(),
 1267:         rpc_call(mod_offline_backend, write_messages, [host_type(), LUser, LServer, [OfflineOld, OfflineNew]]),
 1268:         %% when
 1269:         {_, 0} = mongooseimctl("delete_old_messages", [LServer, "1"], Config),
 1270:         {ok, SecondList} = rpc_call(mod_offline_backend, pop_messages, [HostType, JidRecordBob]),
 1271:         %% then
 1272:         1 = length(SecondList)
 1273:     end).
 1274: 
 1275: remove_expired_messages_test(Config) ->
 1276:     escalus:story(Config, [{mike, 1}], fun(_) ->
 1277:         %% given
 1278:         JidA = nick_to_jid(mike, Config),
 1279:         JidB = nick_to_jid(kate, Config),
 1280:         JidRecordMike = jid:from_binary(JidA),
 1281:         JidRecordKate = jid:from_binary(JidB),
 1282:         Domain = domain(),
 1283:         Msg1 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "Rolling stones"),
 1284:         Msg2 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "Arctic monkeys!"),
 1285:         Msg3 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "More wine..."),
 1286:         Msg4 = escalus_stanza:chat_to(<<"kate@", Domain/binary>>, "kings of leon"),
 1287:         OldTimestamp = fallback_timestamp(10, os:system_time(microsecond)),
 1288:         ExpirationTime = fallback_timestamp(2, os:system_time(microsecond)),
 1289:         ExpirationTimeFuture= fallback_timestamp(-5, os:system_time(microsecond)),
 1290:         OfflineOld = generate_offline_expired_message(JidRecordMike,
 1291:                                                       JidRecordKate, Msg1,
 1292:                                                       OldTimestamp,
 1293:                                                       ExpirationTime),
 1294:         OfflineNow = generate_offline_expired_message(JidRecordMike,
 1295:                              JidRecordKate, Msg2, os:system_time(microsecond), ExpirationTime),
 1296:         OfflineFuture = generate_offline_expired_message(JidRecordMike,
 1297:                              JidRecordKate, Msg3, os:system_time(microsecond), ExpirationTimeFuture),
 1298:         OfflineFuture2 = generate_offline_expired_message(JidRecordMike,
 1299:                                                           JidRecordKate, Msg4,
 1300:                                                           OldTimestamp,
 1301:                                                           ExpirationTimeFuture),
 1302:         {jid, _, _, _, LUser, LServer, _} = JidRecordKate,
 1303:         Args = [OfflineOld, OfflineNow, OfflineFuture, OfflineFuture2],
 1304:         HostType = host_type(),
 1305:         rpc_call(mod_offline_backend, write_messages, [HostType, LUser, LServer, Args]),
 1306:         %% when
 1307:         {_, 0} = mongooseimctl("delete_expired_messages", [LServer], Config),
 1308:         {ok, SecondList} = rpc_call(mod_offline_backend, pop_messages, [HostType, JidRecordKate]),
 1309:         %% then
 1310:         2 = length(SecondList)
 1311:     end).
 1312: 
 1313: %%-----------------------------------------------------------------
 1314: %% Helpers
 1315: %%-----------------------------------------------------------------
 1316: 
 1317: 
 1318: nick_to_jid(UserName, Config) when is_atom(UserName) ->
 1319:     UserSpec = escalus_users:get_userspec(Config, UserName),
 1320:     escalus_utils:jid_to_lower(escalus_users:get_jid(Config, UserSpec)).
 1321: 
 1322: generate_offline_message(From, To, Msg, TimeStamp) ->
 1323:     {jid, _, _, _, LUser, LServer, _} = To,
 1324:     #offline_msg{us = {LUser, LServer}, timestamp = TimeStamp, expire = never,
 1325:                  from = From, to = To, packet = Msg}.
 1326: 
 1327: generate_offline_expired_message(From, To, Msg, TimeStamp, ExpirationTime) ->
 1328:     {jid, _, _, _, LUser, LServer, _} = To,
 1329:     #offline_msg{us = {LUser, LServer}, timestamp = TimeStamp,
 1330:                  expire = ExpirationTime, from = From, to = To, packet = Msg}.
 1331: 
 1332: 
 1333: fallback_timestamp(HowManyDays, TS_MicroSeconds) ->
 1334:     HowManySeconds = HowManyDays * 86400,
 1335:     HowManyMicroSeconds = erlang:convert_time_unit(HowManySeconds, second, microsecond),
 1336:     TS_MicroSeconds - HowManyMicroSeconds.
 1337: 
 1338: get_user_data(User, Config) when is_atom(User) ->
 1339:     get_user_data(escalus_users:get_options(Config, User, <<"newres">>), Config);
 1340: get_user_data(User, _Config) ->
 1341:     {_, Password} = lists:keyfind(password, 1, User),
 1342:     {_, Username} = lists:keyfind(username, 1, User),
 1343:     {_, Domain} = lists:keyfind(server, 1, User),
 1344:     {Username, Domain, Password}.
 1345: 
 1346: get_md5(AccountPass) ->
 1347:     lists:flatten([io_lib:format("~.16B", [X])
 1348:                    || X <- binary_to_list(crypto:hash(md5, AccountPass))]).
 1349: get_sha(AccountPass) ->
 1350:     lists:flatten([io_lib:format("~.16B", [X])
 1351:                    || X <- binary_to_list(crypto:hash(sha, AccountPass))]).
 1352: 
 1353: set_last(User, Domain, TStamp) ->
 1354:     rpc(mim(), mod_last, store_last_info,
 1355:         [host_type(), escalus_utils:jid_to_lower(User), Domain, TStamp, <<>>]).
 1356: 
 1357: delete_users(_Config) ->
 1358:     lists:foreach(fun({User, Domain}) ->
 1359:                 JID = mongoose_helper:make_jid(User, Domain),
 1360:                 rpc(mim(), ejabberd_auth, remove_user, [JID])
 1361:         end, get_registered_users()).
 1362: 
 1363: %%-----------------------------------------------------------------
 1364: %% Predicates
 1365: %%-----------------------------------------------------------------
 1366: 
 1367: match_user_status(Users, StatusTxt) ->
 1368:     Statuses = string:tokens(StatusTxt, "\n"),
 1369: 
 1370:     true = (length(Users) == length(Statuses)),
 1371:     match_user_status2(Users, Statuses).
 1372: 
 1373: match_user_status2([], _) ->
 1374:     true;
 1375: match_user_status2([User | UserR], Statuses) ->
 1376:     Username = binary_to_list(escalus_client:username(User)),
 1377:     Domain = binary_to_list(escalus_client:server(User)),
 1378:     Resource = binary_to_list(escalus_client:resource(User)),
 1379: 
 1380:     true = lists:any(fun(Status) ->
 1381:                 [Username, Domain, Resource]
 1382:                 =:=
 1383:                 lists:sublist(string:tokens(Status, "\t"), 1, 3)
 1384:         end, Statuses),
 1385:     match_user_status2(UserR, Statuses).
 1386: 
 1387: match_user_info(Users, UsersTxt) ->
 1388:     UsersInfo = string:tokens(UsersTxt, "\n"),
 1389:     case length(Users) == length(UsersInfo) of
 1390:         true ->
 1391:             ok;
 1392:         false ->
 1393:             ct:fail(#{what => match_user_info_failed,
 1394:                       users => Users, user_info => UsersInfo})
 1395:     end,
 1396:     match_user_info2(Users, UsersInfo).
 1397: 
 1398: match_user_info2([], _) ->
 1399:     true;
 1400: match_user_info2([User | UserR], UsersInfo) ->
 1401:     Username = binary_to_list(escalus_client:username(User)),
 1402:     Domain = binary_to_list(escalus_client:server(User)),
 1403:     Resource = binary_to_list(escalus_client:resource(User)),
 1404:     FullJID = Username ++ "@" ++ Domain ++ "/" ++ Resource,
 1405: 
 1406:     true = lists:any(fun(UserInfo) ->
 1407:                 string:str(UserInfo, string:to_lower(FullJID)) =:= 1
 1408:         end, UsersInfo),
 1409:     match_user_info2(UserR, UsersInfo).
 1410: 
 1411: match_roster(ItemsValid, Items) ->
 1412:     ItemsTokens = [ string:tokens(ItemToken, "\t") || ItemToken <- string:tokens(Items, "\n") ],
 1413: 
 1414:     true = (length(ItemsValid) == length(ItemsTokens)),
 1415:     true = lists:all(fun({Username, Domain, _Nick, _Group, _Sub}) ->
 1416:                     JID = escalus_utils:jid_to_lower(<<Username/binary, "@", Domain/binary >>),
 1417:                     lists:any(fun
 1418:                                 ([RosterJID, _Nick, _Sub, "none", _Group]) ->
 1419:                                     JID =:= escalus_utils:jid_to_lower(list_to_binary(RosterJID));
 1420:                                 (_) ->
 1421:                                     false
 1422:                               end, ItemsTokens)
 1423:             end, ItemsValid).
 1424: 
 1425: string_to_binary(List) ->
 1426:     case erlang:system_info(otp_release) of
 1427:         [$R|_] ->
 1428:             list_to_binary(List);
 1429:         _ ->
 1430:             unicode:characters_to_binary(List)
 1431:     end.
 1432: 
 1433: add_rosteritem1(UserName1, Domain, UserName2, Config) ->
 1434:     mongooseimctl("add_rosteritem",
 1435:                 [UserName1, Domain, UserName2,
 1436:                  Domain, "MyBob", "MyGroup", "both"], Config).
 1437: 
 1438: add_rosteritem2(Name1, Domain1, Name2, Domain2, Config) ->
 1439:     mongooseimctl("add_rosteritem",
 1440:                 [Name1, Domain1, Name2,
 1441:                  Domain2, "DearMike", "MyGroup", "both"], Config).