1: -module(ejabberd_sm_SUITE).
    2: -include_lib("eunit/include/eunit.hrl").
    3: -include_lib("common_test/include/ct.hrl").
    4: 
    5: -include_lib("jid/include/jid.hrl").
    6: -include_lib("session.hrl").
    7: -compile([export_all, nowarn_export_all]).
    8: 
    9: -define(eq(E, I), ?assertEqual(E, I)).
   10: 
   11: -define(B(C), (proplists:get_value(backend, C))).
   12: -define(MAX_USER_SESSIONS, 2).
   13: 
   14: -import(config_parser_helper, [default_config/1]).
   15: 
   16: all() -> [{group, mnesia}, {group, redis}, {group, cets}].
   17: 
   18: init_per_suite(C) ->
   19:     {ok, _} = application:ensure_all_started(jid),
   20:     application:ensure_all_started(exometer_core),
   21:     F = fun() ->
   22:         ejabberd_sm_backend_sup:start_link(),
   23:         receive stop -> ok end
   24:     end,
   25:     Pid = spawn(F),
   26:     [{pid, Pid} | C].
   27: 
   28: end_per_suite(C) ->
   29:     Pid = ?config(pid, C),
   30:     Pid ! stop,
   31:     application:stop(exometer),
   32:     application:stop(exometer_core).
   33: 
   34: groups() ->
   35:     [{mnesia, [], tests()},
   36:      {redis, [], tests()},
   37:      {cets, [], tests()}].
   38: 
   39: tests() ->
   40:     [open_session,
   41:      get_full_session_list,
   42:      get_vh_session_list,
   43:      get_sessions_2,
   44:      get_sessions_3,
   45:      session_is_updated_when_created_twice,
   46:      delete_session,
   47:      clean_up,
   48:      too_many_sessions,
   49:      unique_count,
   50:      unique_count_while_removing_entries,
   51:      session_info_is_stored,
   52:      session_info_is_updated_if_keys_match,
   53:      session_info_is_updated_properly_if_session_conflicts,
   54:      session_info_is_extended_if_new_keys_present,
   55:      session_info_keys_not_truncated_if_session_opened_with_empty_infolist,
   56:      kv_can_be_stored_for_session,
   57:      kv_can_be_updated_for_session,
   58:      kv_can_be_removed_for_session,
   59:      store_info_sends_message_to_the_session_owner,
   60:      remove_info_sends_message_to_the_session_owner
   61:     ].
   62: 
   63: init_per_group(mnesia, Config) ->
   64:     ok = mnesia:create_schema([node()]),
   65:     ok = mnesia:start(),
   66:     [{backend, ejabberd_sm_mnesia} | Config];
   67: init_per_group(redis, Config) ->
   68:     init_redis_group(is_redis_running(), Config);
   69: init_per_group(cets, Config) ->
   70:     DiscoOpts = #{name => mongoose_cets_discovery, disco_file => "does_not_exist.txt"},
   71:     {ok, Pid} = cets_discovery:start(DiscoOpts),
   72:     [{backend, ejabberd_sm_cets}, {cets_disco_pid, Pid} | Config].
   73: 
   74: init_redis_group(true, Config) ->
   75:     Self = self(),
   76:     proc_lib:spawn(fun() ->
   77:                   register(test_helper, self()),
   78:                   mongoose_wpool:ensure_started(),
   79:                   % This would be started via outgoing_pools in normal case
   80:                   Pool = default_config([outgoing_pools, redis, default]),
   81:                   mongoose_wpool:start_configured_pools([Pool], [], []),
   82:                   Self ! ready,
   83:                   receive stop -> ok end
   84:           end),
   85:     receive ready -> ok after timer:seconds(30) -> ct:fail(test_helper_not_ready) end,
   86:     [{backend, ejabberd_sm_redis} | Config];
   87: init_redis_group(_, _) ->
   88:     {skip, "redis not running"}.
   89: 
   90: end_per_group(mnesia, Config) ->
   91:     mnesia:stop(),
   92:     mnesia:delete_schema([node()]),
   93:     Config;
   94: end_per_group(cets, Config) ->
   95:     exit(proplists:get_value(cets_disco_pid, Config), kill),
   96:     Config;
   97: end_per_group(redis, Config) ->
   98:     whereis(test_helper) ! stop,
   99:     Config.
  100: 
  101: init_per_testcase(too_many_sessions, Config) ->
  102:     set_test_case_meck(?MAX_USER_SESSIONS, true),
  103:     setup_sm(Config),
  104:     Config;
  105: init_per_testcase(Case, Config) ->
  106:     set_test_case_meck(infinity, should_meck_c2s(Case)),
  107:     setup_sm(Config),
  108:     Config.
  109: 
  110: should_meck_c2s(store_info_sends_message_to_the_session_owner) -> false;
  111: should_meck_c2s(remove_info_sends_message_to_the_session_owner) -> false;
  112: should_meck_c2s(_) -> true.
  113: 
  114: end_per_testcase(_, Config) ->
  115:     clean_sessions(Config),
  116:     terminate_sm(),
  117:     unload_meck(),
  118:     mongoose_config:erase_opts().
  119: 
  120: open_session(C) ->
  121:     {Sid, USR} = generate_random_user(<<"localhost">>),
  122:     given_session_opened(Sid, USR),
  123:     verify_session_opened(C, Sid, USR).
  124: 
  125: get_full_session_list(C) ->
  126:     ManyUsers = generate_many_random_users(5, [<<"localhost">>, <<"otherhost">>]),
  127:     ManyUsersLen = length(ManyUsers),
  128:     [given_session_opened(Sid, USR) || {Sid, USR} <- ManyUsers],
  129:     AllSessions = ejabberd_sm:get_full_session_list(),
  130:     AllSessionsLen = length(AllSessions),
  131:     AllSessionsLen = ManyUsersLen,
  132:     [verify_session_opened(C, Sid, USR) || {Sid, USR} <- ManyUsers].
  133: 
  134: get_vh_session_list(C) ->
  135:     ManyUsersLocal = generate_many_random_users(5, [<<"localhost">>]),
  136:     ManyUsersOther = generate_many_random_users(5, [<<"otherhost">>]),
  137:     ManyUsersLocalLen = length(ManyUsersLocal),
  138:     [given_session_opened(Sid, USR) || {Sid, USR} <- ManyUsersLocal ++ ManyUsersOther],
  139:     LocalhostSessions = ejabberd_sm:get_vh_session_list(<<"localhost">>),
  140:     LocalhostSessionsLen = length(LocalhostSessions),
  141:     LocalhostSessionsLen = ManyUsersLocalLen,
  142:     ManyUsersLocalLen = ejabberd_sm:get_vh_session_number(<<"localhost">>),
  143:     [verify_session_opened(C, Sid, USR) || {Sid, USR} <- ManyUsersLocal].
  144: 
  145: get_sessions_2(C) ->
  146:     UsersWithManyResources = generate_many_random_res(5, 3, [<<"localhost">>, <<"otherhost">>]),
  147:     [given_session_opened(Sid, USR) || {Sid, USR} <- UsersWithManyResources],
  148:     USDict = get_unique_us_dict(UsersWithManyResources),
  149:     [verify_session_opened(C, U, S, dict:fetch({U, S}, USDict)) || {U, S} <- dict:fetch_keys(USDict)],
  150:     [verify_session_opened(C, Sid, USR) || {Sid, USR} <- UsersWithManyResources].
  151: 
  152: 
  153: get_sessions_3(C) ->
  154:     UserRes = generate_many_random_res(1, 3, [<<"localhost">>]),
  155:     AllSessions = length(UserRes),
  156:     {_, {User, Server, _}} = hd(UserRes),
  157:     [given_session_opened(Sid, USR) || {Sid, USR} <- UserRes],
  158:     Sessions_2 = ?B(C):get_sessions(User, Server),
  159:     AllSessions = length(Sessions_2),
  160:     F = fun({Sid, {U, S, R} = USR}) ->
  161:         [#session{sid = Sid} = Session] = ?B(C):get_sessions(U, S, R),
  162:         Session = lists:keyfind(Sid, #session.sid, Sessions_2),
  163:         Session = lists:keyfind(USR, #session.usr, Sessions_2),
  164:         true
  165:     end,
  166:     true = lists:all(F, UserRes).
  167: 
  168: session_is_updated_when_created_twice(C) ->
  169:     {Sid, {U, S, _} = USR} = generate_random_user(<<"localhost">>),
  170:     given_session_opened(Sid, USR),
  171:     verify_session_opened(C, Sid, USR),
  172: 
  173:     given_session_opened(Sid, USR, 20),
  174:     verify_session_opened(C, Sid, USR),
  175: 
  176:     [#session{usr = USR, sid = Sid, priority = 20}] = ?B(C):get_sessions(),
  177:     [#session{usr = USR, sid = Sid, priority = 20}] = ?B(C):get_sessions(S),
  178:     [#session{priority = 20}] = ?B(C):get_sessions(U, S).
  179: 
  180: session_info_is_stored(C) ->
  181:     {Sid, {U, S, _} = USR} = generate_random_user(<<"localhost">>),
  182:     given_session_opened(Sid, USR, 1, [{key1, val1}]),
  183: 
  184:     [#session{sid = Sid, info = #{key1 := val1}}]
  185:      = ?B(C):get_sessions(U,S).
  186: 
  187: session_info_is_updated_if_keys_match(C) ->
  188:     {Sid, {U, S, _} = USR} = generate_random_user(<<"localhost">>),
  189:     given_session_opened(Sid, USR, 1, [{key1, val1}]),
  190: 
  191:     when_session_opened(Sid, USR, 1, [{key1, val2}]),
  192: 
  193:     [#session{sid = Sid, info = #{key1 := val2}}]
  194:      = ?B(C):get_sessions(U,S).
  195: 
  196: %% Same resource but different sids
  197: session_info_is_updated_properly_if_session_conflicts(C) ->
  198:     %% We use 2 SIDs here
  199:     {Sid, {U, S, _} = USR} = generate_random_user(<<"localhost">>),
  200:     %% Sid2 > Sid in this case, because SIDs have a timestamp in them
  201:     %% We cannot test store_info for Sid2 though, because it has a different pid
  202:     Sid2 = make_sid(),
  203: 
  204:     %% Two sessions for the same USR are registered after that:
  205:     given_session_opened(Sid, USR, 1, [{key1, val1}, {key2, a}]),
  206:     given_session_opened(Sid2, USR, 1, [{key1, val2}, {key3, b}]),
  207: 
  208:     %% Each call to open_session overwrites the previous data without merging.
  209:     %% The current version of mongoose_c2s calls open_session only once per SID.
  210:     %% Still, we want to test what happens if we call open_session the second time.
  211:     when_session_opened(Sid, USR, 1, [{key1, val3}, {key4, c}]),
  212: 
  213:     [#session{sid = Sid, info = Info1}, #session{sid = Sid2, info = Info2}]
  214:         = lists:keysort(#session.sid, ?B(C):get_sessions(U, S)),
  215:     [{key1, val3}, {key4, c}] = maps:to_list(Info1),
  216:     [{key1, val2}, {key3, b}] = maps:to_list(Info2).
  217: 
  218: session_info_is_extended_if_new_keys_present(C) ->
  219:     {Sid, {U, S, _} = USR} = generate_random_user(<<"localhost">>),
  220:     given_session_opened(Sid, USR, 1, [{key1, val1}]),
  221: 
  222:     when_session_opened(Sid, USR, 1, [{key1, val1}, {key2, val2}]),
  223: 
  224:     [#session{sid = Sid, info = #{key1 := val1, key2 := val2}}]
  225:      = ?B(C):get_sessions(U,S).
  226: 
  227: session_info_keys_not_truncated_if_session_opened_with_empty_infolist(C) ->
  228:     {Sid, {U, S, _} = USR} = generate_random_user(<<"localhost">>),
  229:     given_session_opened(Sid, USR, 1, [{key1, val1}]),
  230: 
  231:     %% Should not be called twice in the real life
  232:     when_session_opened(Sid, USR, 1, []),
  233: 
  234:     [#session{sid = Sid, info = #{}}]
  235:      = ?B(C):get_sessions(U,S).
  236: 
  237: 
  238: kv_can_be_stored_for_session(C) ->
  239:     {Sid, {U, S, R} = USR} = generate_random_user(<<"localhost">>),
  240:     given_session_opened(Sid, USR, 1, [{key1, val1}]),
  241:     when_session_info_stored(Sid, U, S, R, {key2, newval}),
  242:     ?assertMatch([#session{sid = Sid, info = #{key1 := val1, key2 := newval}}],
  243:                  ?B(C):get_sessions(U,S)).
  244: 
  245: kv_can_be_updated_for_session(C) ->
  246:     {Sid, {U, S, R} = USR} = generate_random_user(<<"localhost">>),
  247:     given_session_opened(Sid, USR, 1, [{key1, val1}]),
  248: 
  249:     when_session_info_stored(Sid, U, S, R, {key2, newval}),
  250:     when_session_info_stored(Sid, U, S, R, {key2, override}),
  251: 
  252:     ?assertMatch([#session{sid = Sid, info = #{key1 := val1, key2 := override}}],
  253:                  ?B(C):get_sessions(U, S)).
  254: 
  255: kv_can_be_removed_for_session(C) ->
  256:     {Sid, {U, S, R} = USR} = generate_random_user(<<"localhost">>),
  257:     given_session_opened(Sid, USR, 1, [{key1, val1}]),
  258: 
  259:     when_session_info_stored(Sid, U, S, R, {key2, newval}),
  260: 
  261:     [#session{sid = Sid, info = #{key1 := val1, key2 := newval}}]
  262:      = ?B(C):get_sessions(U, S),
  263: 
  264:     when_session_info_removed(Sid, U, S, R, key2),
  265: 
  266:     [#session{sid = Sid, info = #{key1 := val1}}]
  267:      = ?B(C):get_sessions(U, S),
  268: 
  269:     when_session_info_removed(Sid, U, S, R, key1),
  270: 
  271:     [#session{sid = Sid, info = #{}}]
  272:      = ?B(C):get_sessions(U, S).
  273: 
  274: store_info_sends_message_to_the_session_owner(C) ->
  275:     SID = {erlang:system_time(microsecond), self()},
  276:     U = <<"alice2">>,
  277:     S = <<"localhost">>,
  278:     R = <<"res1">>,
  279:     Session = #session{sid = SID, usr = {U, S, R}, us = {U, S}, priority = 1, info = #{}},
  280:     %% Create session in one process
  281:     ?B(C):set_session(U, S, R, Session),
  282:     %% but call store_info from another process
  283:     JID = jid:make_noprep(U, S, R),
  284:     spawn_link(fun() -> ejabberd_sm:store_info(JID, SID, cc, undefined) end),
  285:     %% The original process receives a message
  286:     receive
  287:         {'$gen_cast', {async_with_state,
  288:                        _Fun,
  289:                        [SID, #jid{luser = User, lserver = Server, lresource = Resource},
  290:                         K, V]}} ->
  291:             ?eq(U, User),
  292:             ?eq(S, Server),
  293:             ?eq(R, Resource),
  294:             ?eq({cc, undefined}, {K, V}),
  295:             ok;
  296:         Message ->
  297:             ct:fail("unexpected message: ~p", [Message])
  298:     after 5000 ->
  299:         ct:fail("store_info_sends_message_to_the_session_owner=timeout")
  300:     end.
  301: 
  302: remove_info_sends_message_to_the_session_owner(C) ->
  303:     SID = {erlang:timestamp(), self()},
  304:     U = <<"alice2">>,
  305:     S = <<"localhost">>,
  306:     R = <<"res1">>,
  307:     Session = #session{sid = SID, usr = {U, S, R}, us = {U, S}, priority = 1, info = #{}},
  308:     %% Create session in one process
  309:     ?B(C):set_session(U, S, R, Session),
  310:     %% but call remove_info from another process
  311:     JID = jid:make_noprep(U, S, R),
  312:     spawn_link(fun() -> ejabberd_sm:remove_info(JID, SID, cc) end),
  313:     %% The original process receives a message
  314:     receive
  315:         {'$gen_cast', {async_with_state,
  316:                        _Fun,
  317:                        [SID, #jid{luser = User, lserver = Server, lresource = Resource},
  318:                         Key, undefined]}} ->
  319:             ?eq(U, User),
  320:             ?eq(S, Server),
  321:             ?eq(R, Resource),
  322:             ?eq(cc, Key),
  323:             ok;
  324:         Message ->
  325:             ct:fail("unexpected message: ~p", [Message])
  326:     after 5000 ->
  327:         ct:fail("remove_info_sends_message_to_the_session_owner=timeout")
  328:     end.
  329: 
  330: delete_session(C) ->
  331:     {Sid, {U, S, R} = USR} = generate_random_user(<<"localhost">>),
  332:     given_session_opened(Sid, USR),
  333:     verify_session_opened(C, Sid, USR),
  334: 
  335:     ?B(C):delete_session(Sid, U, S, R),
  336: 
  337:     [] = ?B(C):get_sessions(),
  338:     [] = ?B(C):get_sessions(S),
  339:     [] = ?B(C):get_sessions(U, S),
  340:     [] = ?B(C):get_sessions(U, S, R).
  341: 
  342: 
  343: 
  344: clean_up(C) ->
  345:     UsersWithManyResources = generate_many_random_res(5, 3, [<<"localhost">>, <<"otherhost">>]),
  346:     [given_session_opened(Sid, USR) || {Sid, USR} <- UsersWithManyResources],
  347:     ?B(C):cleanup(node()),
  348:     %% give sm backend some time to clean all sessions
  349:     ensure_empty(C, 10, ?B(C):get_sessions()).
  350: 
  351: ensure_empty(_C, 0, Sessions) ->
  352:     [] = Sessions;
  353: ensure_empty(C, N, Sessions) ->
  354:     case Sessions of
  355:         [] ->
  356:             ok;
  357:         _ ->
  358:             timer:sleep(50),
  359:             ensure_empty(C, N-1, ?B(C):get_sessions())
  360:     end.
  361: 
  362: too_many_sessions(_C) ->
  363:     %% Max sessions set to ?MAX_USER_SESSIONS in init_per_testcase
  364:     UserSessions = [generate_random_user(<<"a">>, <<"localhost">>) ||
  365:                        _ <- lists:seq(1, ?MAX_USER_SESSIONS + 1)],
  366:     [given_session_opened(Sid, USR) || {Sid, USR} <- UserSessions],
  367: 
  368:     receive
  369:         {forwarded, _Sid, {'$gen_cast', {exit, <<"Replaced by new connection">>}}} ->
  370:             ok;
  371:         Message ->
  372:             ct:fail("Unexpected message: ~p", [Message])
  373:     after 10 ->
  374:         ct:fail("replaced message not sent")
  375:     end.
  376: 
  377: 
  378: 
  379: unique_count(_C) ->
  380:     UsersWithManyResources = generate_many_random_res(5, 3, [<<"localhost">>, <<"otherhost">>]),
  381:     [given_session_opened(Sid, USR) || {Sid, USR} <- UsersWithManyResources],
  382:     USDict = get_unique_us_dict(UsersWithManyResources),
  383:     UniqueCount = ejabberd_sm:get_unique_sessions_number(),
  384:     UniqueCount = dict:size(USDict).
  385: 
  386: 
  387: unique_count_while_removing_entries(C) ->
  388:     unique_count(C),
  389:     UniqueCount = ejabberd_sm:get_unique_sessions_number(),
  390:     %% Register more sessions and mock the crash
  391:     UsersWithManyResources = generate_many_random_res(10, 3, [<<"localhost">>, <<"otherhost">>]),
  392:     [given_session_opened(Sid, USR) || {Sid, USR} <- UsersWithManyResources],
  393:     set_test_case_meck_unique_count_crash(?B(C)),
  394:     USDict = get_unique_us_dict(UsersWithManyResources),
  395:     %% Check if unique count equals prev cached value
  396:     UniqueCount = ejabberd_sm:get_unique_sessions_number(),
  397:     meck:unload(?B(C)),
  398:     true = UniqueCount /= dict:size(USDict) + UniqueCount.
  399: 
  400: unload_meck() ->
  401:     meck:unload(acl),
  402:     meck:unload(gen_hook),
  403:     meck:unload(mongoose_domain_api),
  404:     catch ets:delete(test_c2s_info),
  405:     catch meck:unload(mongoose_c2s).
  406: 
  407: set_test_case_meck(MaxUserSessions, MeckC2s) ->
  408:     ets:new(test_c2s_info, [public, named_table]),
  409:     meck:new(acl, []),
  410:     meck:expect(acl, match_rule, fun(_, _, _, _) -> MaxUserSessions end),
  411:     meck:new(gen_hook, []),
  412:     meck:expect(gen_hook, run_fold, fun(_, _, Acc, _) -> {ok, Acc} end),
  413:     meck:new(mongoose_domain_api, []),
  414:     meck:expect(mongoose_domain_api, get_domain_host_type, fun(H) -> {ok, H} end),
  415:     do_meck_c2s(MeckC2s).
  416: 
  417: do_meck_c2s(false) ->
  418:     ok;
  419: do_meck_c2s(true) ->
  420:     %% Very simple mock, not even reproducing async behaviour
  421:     %% It is for a limited use only, for more complex tests use a real c2s process (i.e. probably big tests)
  422:     meck:new(mongoose_c2s, [passthrough]),
  423:     meck:expect(mongoose_c2s, async_with_state, fun async_with_state/3),
  424:     meck:expect(mongoose_c2s, get_info, fun get_info/1),
  425:     meck:expect(mongoose_c2s, set_info, fun set_info/2),
  426:     %% Just return same thing all the time
  427:     meck:expect(mongoose_c2s, get_mod_state, fun(_C2sState, _Mod) -> {error, not_found} end).
  428: 
  429: async_with_state(Pid, Fun, Args) ->
  430:     apply(Fun, [{ministate, Pid}|Args]),
  431:     ok.
  432: 
  433: get_info({ministate, Pid}) ->
  434:     case ets:lookup(test_c2s_info, Pid) of
  435:         [] ->
  436:             #{};
  437:         [{Pid, Info}] ->
  438:             Info
  439:     end.
  440: 
  441: set_info({ministate, Pid} = S, Info) ->
  442:     ets:insert(test_c2s_info, {Pid, Info}),
  443:     S.
  444: 
  445: set_test_case_meck_unique_count_crash(Backend) ->
  446:     F = get_fun_for_unique_count(Backend),
  447:     meck:new(Backend, []),
  448:     meck:expect(Backend, unique_count, F).
  449: 
  450: get_fun_for_unique_count(ejabberd_sm_mnesia) ->
  451:     fun() ->
  452:         mnesia:abort({badarg,[session,{{1442,941593,580189},list_to_pid("<0.23291.6>")}]})
  453:     end;
  454: get_fun_for_unique_count(ejabberd_sm_cets) ->
  455:     fun() -> error(oops) end;
  456: get_fun_for_unique_count(ejabberd_sm_redis) ->
  457:     fun() ->
  458:         %% The code below is on purpose, it's to crash with badarg reason
  459:         length({error, timeout})
  460:     end.
  461: 
  462: make_sid() ->
  463:     %% A sid consists of a timestamp and a pid.
  464:     %% Timestamps can repeat, and a unique pid is needed to avoid sid duplication.
  465:     TestPid = self(),
  466:     Pid = spawn_link(fun() ->
  467:                              Sid = ejabberd_sm:make_new_sid(),
  468:                              TestPid ! {sid, Sid},
  469:                              forward_messages(Sid, TestPid)
  470:                      end),
  471:     receive
  472:         {sid, Sid = {_, Pid}} -> Sid
  473:     after
  474:         1000 -> ct:fail("Timeout waiting for sid")
  475:     end.
  476: 
  477: forward_messages(Sid, Target) ->
  478:     receive
  479:         Msg -> Target ! {forwarded, Sid, Msg}
  480:     end,
  481:     forward_messages(Sid, Target).
  482: 
  483: given_session_opened(Sid, USR) ->
  484:     given_session_opened(Sid, USR, 1).
  485: 
  486: given_session_opened(Sid, {U, S, R}, Priority) ->
  487:     given_session_opened(Sid, {U, S, R}, Priority, []).
  488: 
  489: given_session_opened(Sid, {U, S, R}, Priority, Info) ->
  490:     {_, Pid} = Sid,
  491:     Map = maps:from_list(Info),
  492:     %% open_session is called by c2s usually, but here in tests the function is called by the test.
  493:     %% we still need to remember the initial info for tests to work though.
  494:     ets:insert_new(test_c2s_info, {Pid, Map}),
  495:     JID = jid:make_noprep(U, S, R),
  496:     ejabberd_sm:open_session(S, Sid, JID, Priority, Map).
  497: 
  498: when_session_opened(Sid, {U, S, R}, Priority, Info) ->
  499:     given_session_opened(Sid, {U, S, R}, Priority, Info).
  500: 
  501: when_session_info_stored(SID, U, S, R, {K, V}) ->
  502:     JID = jid:make_noprep(U, S, R),
  503:     ejabberd_sm:store_info(JID, SID, K, V).
  504: 
  505: when_session_info_removed(SID, U, S, R, Key) ->
  506:     JID = jid:make_noprep(U, S, R),
  507:     ejabberd_sm:remove_info(JID, SID, Key).
  508: 
  509: verify_session_opened(C, Sid, USR) ->
  510:     do_verify_session_opened(?B(C), Sid, USR).
  511: 
  512: do_verify_session_opened(ejabberd_sm_mnesia, Sid, {U, S, R} = USR) ->
  513:     general_session_check(ejabberd_sm_mnesia, Sid, USR, U, S, R);
  514: do_verify_session_opened(ejabberd_sm_cets, Sid, {U, S, R} = USR) ->
  515:     general_session_check(ejabberd_sm_cets, Sid, USR, U, S, R);
  516: do_verify_session_opened(ejabberd_sm_redis, Sid, {U, S, R} = USR) ->
  517:     UHash = iolist_to_binary(hash(U, S, R, Sid)),
  518:     Hashes = mongoose_redis:cmd(["SMEMBERS", n(node())]),
  519:     true = lists:member(UHash, Hashes),
  520:     SessionsUSEncoded = mongoose_redis:cmd(["SMEMBERS", hash(U, S)]),
  521:     SessionsUS = [binary_to_term(Entry) || Entry <- SessionsUSEncoded],
  522:     true = lists:keymember(Sid, 2, SessionsUS),
  523:     [SessionUSREncoded] = mongoose_redis:cmd(["SMEMBERS", hash(U, S, R)]),
  524:     SessionUSR = binary_to_term(SessionUSREncoded),
  525:     #session{sid = Sid} = SessionUSR,
  526:     general_session_check(ejabberd_sm_redis, Sid, USR, U, S, R).
  527: 
  528: verify_session_opened(C, U, S, Resources) ->
  529:     Sessions = ?B(C):get_sessions(U, S),
  530:     F = fun({Sid, USR}) ->
  531:         #session{} = Session = lists:keyfind(Sid, #session.sid, Sessions),
  532:         Session == lists:keyfind(USR, #session.usr, Sessions)
  533:     end,
  534:     true = lists:all(F, Resources).
  535: 
  536: general_session_check(M, Sid, USR, U, S, R) ->
  537:     [#session{sid = Sid, usr = USR, us = {U, S}}] = M:get_sessions(U, S, R).
  538: 
  539: clean_sessions(C) ->
  540:     case ?B(C) of
  541:         ejabberd_sm_mnesia ->
  542:             mnesia:clear_table(session);
  543:         ejabberd_sm_redis ->
  544:             mongoose_redis:cmd(["FLUSHALL"]);
  545:         ejabberd_sm_cets ->
  546:             ets:delete_all_objects(cets_session)
  547:     end.
  548: 
  549: generate_random_user(S) ->
  550:     U = base16:encode(crypto:strong_rand_bytes(5)),
  551:     generate_random_user(U, S).
  552: 
  553: generate_random_user(U, S) ->
  554:     R = base16:encode(crypto:strong_rand_bytes(5)),
  555:     generate_user(U, S, R).
  556: 
  557: generate_user(U, S, R) ->
  558:     Sid = make_sid(),
  559:     {Sid, {U, S, R}}.
  560: 
  561: generate_many_random_users(PerServerCount, Servers) ->
  562:     Users = [generate_random_users(PerServerCount, Server) || Server <- Servers],
  563:     lists:flatten(Users).
  564: 
  565: generate_random_users(Count, Server) ->
  566:     [generate_random_user(Server) || _ <- lists:seq(1, Count)].
  567: 
  568: generate_many_random_res(UsersPerServer, ResourcesPerUser, Servers) ->
  569:     Usernames = [base16:encode(crypto:strong_rand_bytes(5)) || _ <- lists:seq(1, UsersPerServer)],
  570:     [generate_random_user(U, S) || U <- Usernames, S <- Servers, _ <- lists:seq(1, ResourcesPerUser)].
  571: 
  572: get_unique_us_dict(USRs) ->
  573:     F = fun({_, {U, S, _}} = I, SetAcc) ->
  574:         dict:append({U, S}, I, SetAcc)
  575:     end,
  576:     lists:foldl(F, dict:new(), USRs).
  577: 
  578: %% Taken from ejabberd_sm_redis
  579: 
  580: -spec hash(binary()) -> iolist().
  581: hash(Val1) ->
  582:     ["s3:*:", Val1, ":*"].
  583: 
  584: 
  585: -spec hash(binary(), binary()) -> iolist().
  586: hash(Val1, Val2) ->
  587:     ["s2:", Val1, ":", Val2].
  588: 
  589: 
  590: -spec hash(binary(), binary(), binary()) -> iolist().
  591: hash(Val1, Val2, Val3) ->
  592:     ["s3:", Val1, ":", Val2, ":", Val3].
  593: 
  594: 
  595: -spec hash(binary(), binary(), binary(), binary()) -> iolist().
  596: hash(Val1, Val2, Val3, Val4) ->
  597:     ["s4:", Val1, ":", Val2, ":", Val3, ":", term_to_binary(Val4)].
  598: 
  599: 
  600: -spec n(atom()) -> iolist().
  601: n(Node) ->
  602:     ["n:", atom_to_list(Node)].
  603: 
  604: 
  605: is_redis_running() ->
  606:     case eredis:start_link([{host, "127.0.0.1"}]) of
  607:         {ok, Client} ->
  608:             Result = eredis:q(Client, [<<"PING">>], 5000),
  609:             eredis:stop(Client),
  610:             case Result of
  611:                 {ok,<<"PONG">>} ->
  612:                     true;
  613:                 _ ->
  614:                     false
  615:             end;
  616:         _ ->
  617:             false
  618:     end.
  619: 
  620: setup_sm(Config) ->
  621:     mongoose_config:set_opts(opts(Config)),
  622:     set_meck(),
  623:     ejabberd_sm:start_link(),
  624:     case ?config(backend, Config) of
  625:         ejabberd_sm_redis ->
  626:             mongoose_redis:cmd(["FLUSHALL"]);
  627:         ejabberd_sm_mnesia ->
  628:             ok;
  629:         ejabberd_sm_cets ->
  630:             ok
  631:     end.
  632: 
  633: terminate_sm() ->
  634:     gen_server:stop(ejabberd_sm).
  635: 
  636: opts(Config) ->
  637:     #{hosts => [<<"localhost">>],
  638:       host_types => [],
  639:       all_metrics_are_global => false,
  640:       sm_backend => sm_backend(?config(backend, Config))}.
  641: 
  642: sm_backend(ejabberd_sm_redis) -> redis;
  643: sm_backend(ejabberd_sm_mnesia) -> mnesia;
  644: sm_backend(ejabberd_sm_cets) -> cets.
  645: 
  646: set_meck() ->
  647:     meck:expect(gen_hook, add_handler, fun(_, _, _, _, _) -> ok end),
  648:     meck:expect(gen_hook, add_handlers, fun(_) -> ok end),
  649:     ok.