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