1: %%%===================================================================
    2: %%% @copyright (C) 2016, Erlang Solutions Ltd.
    3: %%% @doc Suite for testing Personal Eventing Protocol features
    4: %%%      as described in XEP-0163
    5: %%% @end
    6: %%%===================================================================
    7: 
    8: -module(pep_SUITE).
    9: 
   10: -include_lib("escalus/include/escalus.hrl").
   11: -include_lib("common_test/include/ct.hrl").
   12: -include_lib("escalus/include/escalus_xmlns.hrl").
   13: -include_lib("exml/include/exml.hrl").
   14: -include_lib("exml/include/exml_stream.hrl").
   15: -include_lib("eunit/include/eunit.hrl").
   16: 
   17: -export([suite/0, all/0, groups/0]).
   18: -export([init_per_suite/1, end_per_suite/1,
   19:          init_per_group/2, end_per_group/2,
   20:          init_per_testcase/2, end_per_testcase/2]).
   21: 
   22: -export([
   23:          disco_test/1,
   24:          disco_sm_test/1,
   25:          disco_sm_items_test/1,
   26:          pep_caps_test/1,
   27:          publish_and_notify_test/1,
   28:          publish_options_test/1,
   29:          send_caps_after_login_test/1,
   30:          delayed_receive/1,
   31:          delayed_receive_with_sm/1,
   32:          h_ok_after_notify_test/1,
   33:          authorize_access_model/1,
   34:          unsubscribe_after_presence_unsubscription/1
   35:         ]).
   36: 
   37: -export([
   38:          send_initial_presence_with_caps/2
   39:         ]).
   40: 
   41: -import(distributed_helper, [mim/0,
   42:                              require_rpc_nodes/1,
   43:                              subhost_pattern/1,
   44:                              rpc/4]).
   45: 
   46: -import(domain_helper, [domain/0]).
   47: 
   48: %%--------------------------------------------------------------------
   49: %% Suite configuration
   50: %%--------------------------------------------------------------------
   51: 
   52: all() ->
   53:     [
   54:      {group, pep_tests}
   55:     ].
   56: 
   57: groups() ->
   58:     G = [
   59:          {pep_tests, [parallel],
   60:           [
   61:            disco_test,
   62:            disco_sm_test,
   63:            disco_sm_items_test,
   64:            pep_caps_test,
   65:            publish_and_notify_test,
   66:            publish_options_test,
   67:            send_caps_after_login_test,
   68:            delayed_receive,
   69:            delayed_receive_with_sm,
   70:            h_ok_after_notify_test,
   71:            authorize_access_model,
   72:            unsubscribe_after_presence_unsubscription
   73:           ]
   74:          },
   75:          {cache_tests, [parallel],
   76:           [
   77:            send_caps_after_login_test,
   78:            delayed_receive,
   79:            delayed_receive_with_sm,
   80:            unsubscribe_after_presence_unsubscription
   81:           ]
   82:          }
   83:         ],
   84:     ct_helper:repeat_all_until_all_ok(G).
   85: 
   86: suite() ->
   87:     require_rpc_nodes([mim]) ++ escalus:suite().
   88: 
   89: %%--------------------------------------------------------------------
   90: %% Init & teardown
   91: %%--------------------------------------------------------------------
   92: 
   93: init_per_suite(Config) ->
   94:     escalus:init_per_suite(dynamic_modules:save_modules(domain(), Config)).
   95: 
   96: end_per_suite(Config) ->
   97:     escalus_fresh:clean(),
   98:     dynamic_modules:restore_modules(Config),
   99:     escalus:end_per_suite(Config).
  100: 
  101: init_per_group(cache_tests, Config) ->
  102:     Config0 = dynamic_modules:save_modules(domain(), Config),
  103:     NewConfig =  required_modules(cache_tests),
  104:     dynamic_modules:ensure_modules(domain(), NewConfig),
  105:     Config0;
  106: 
  107: init_per_group(_GroupName, Config) ->
  108:     dynamic_modules:ensure_modules(domain(), required_modules()),
  109:     Config.
  110: 
  111: end_per_group(cache_tests, Config) ->
  112:     dynamic_modules:restore_modules(Config);
  113: 
  114: end_per_group(_GroupName, Config) ->
  115:     Config.
  116: 
  117: init_per_testcase(TestName, Config) ->
  118:     escalus:init_per_testcase(TestName, Config).
  119: 
  120: end_per_testcase(TestName, Config) ->
  121:     escalus:end_per_testcase(TestName, Config).
  122: 
  123: %%--------------------------------------------------------------------
  124: %% Test cases for XEP-0163
  125: %% Comments in test cases refer to sections is the XEP
  126: %%--------------------------------------------------------------------
  127: 
  128: %% Group: pep_tests (sequence)
  129: 
  130: disco_test(Config) ->
  131:     escalus:fresh_story(
  132:       Config,
  133:       [{alice, 1}],
  134:       fun(Alice) ->
  135:               escalus:send(Alice, escalus_stanza:disco_info(pubsub_tools:node_addr())),
  136:               Stanza = escalus:wait_for_stanza(Alice),
  137:               escalus:assert(has_identity, [<<"pubsub">>, <<"service">>], Stanza),
  138:               escalus:assert(has_identity, [<<"pubsub">>, <<"pep">>], Stanza),
  139:               escalus:assert(has_feature, [?NS_PUBSUB], Stanza)
  140:       end).
  141: 
  142: disco_sm_test(Config) ->
  143:     escalus:fresh_story(
  144:       Config,
  145:       [{alice, 1}],
  146:       fun(Alice) ->
  147:               AliceJid = escalus_client:short_jid(Alice),
  148:               escalus:send(Alice, escalus_stanza:disco_info(AliceJid)),
  149:               Stanza = escalus:wait_for_stanza(Alice),
  150:               ?assertNot(escalus_pred:has_identity(<<"pubsub">>, <<"service">>, Stanza)),
  151:               escalus:assert(has_identity, [<<"pubsub">>, <<"pep">>], Stanza),
  152:               escalus:assert(has_feature, [?NS_PUBSUB], Stanza),
  153:               escalus:assert(is_stanza_from, [AliceJid], Stanza)
  154:       end).
  155: 
  156: disco_sm_items_test(Config) ->
  157:     NodeNS = random_node_ns(),
  158:     escalus:fresh_story(
  159:       set_caps_node(NodeNS, Config),
  160:       [{alice, 1}],
  161:       fun(Alice) ->
  162:               AliceJid = escalus_client:short_jid(Alice),
  163: 
  164:               %% Node not present yet
  165:               escalus:send(Alice, escalus_stanza:disco_items(AliceJid)),
  166:               Stanza1 = escalus:wait_for_stanza(Alice),
  167:               Query1 = exml_query:subelement(Stanza1, <<"query">>),
  168:               ?assertEqual(undefined, exml_query:subelement_with_attr(Query1, <<"node">>, NodeNS)),
  169:               escalus:assert(is_stanza_from, [AliceJid], Stanza1),
  170: 
  171:               %% Publish an item to trigger node creation
  172:               pubsub_tools:publish(Alice, <<"item1">>, {pep, NodeNS}, []),
  173: 
  174:               %% Node present
  175:               escalus:send(Alice, escalus_stanza:disco_items(AliceJid)),
  176:               Stanza2 = escalus:wait_for_stanza(Alice),
  177:               Query2 = exml_query:subelement(Stanza2, <<"query">>),
  178:               Item = exml_query:subelement_with_attr(Query2, <<"node">>, NodeNS),
  179:               ?assertEqual(jid:str_tolower(AliceJid), exml_query:attr(Item, <<"jid">>)),
  180:               escalus:assert(is_stanza_from, [AliceJid], Stanza2)
  181:       end).
  182: 
  183: pep_caps_test(Config) ->
  184:     escalus:fresh_story(
  185:       Config,
  186:       [{bob, 1}],
  187:       fun(Bob) ->
  188:               NodeNS = random_node_ns(),
  189:               Caps = caps(NodeNS),
  190: 
  191:               %% Send presence with capabilities (chap. 1 ex. 4)
  192:               %% Server does not know the version string, so it requests feature list
  193:               send_presence_with_caps(Bob, Caps),
  194:               DiscoRequest = escalus:wait_for_stanza(Bob),
  195: 
  196:               %% Client responds with a list of supported features (chap. 1 ex. 5)
  197:               send_caps_disco_result(Bob, DiscoRequest, NodeNS),
  198: 
  199:               receive_presence_with_caps(Bob, Bob, Caps)
  200:       end).
  201: 
  202: publish_and_notify_test(Config) ->
  203:     NodeNS = random_node_ns(),
  204:     escalus:fresh_story(
  205:       set_caps_node(NodeNS, Config),
  206:       [{alice, 1}, {bob, 1}],
  207:       fun(Alice, Bob) ->
  208:               escalus_story:make_all_clients_friends([Alice, Bob]),
  209: 
  210:               pubsub_tools:publish(Alice, <<"item1">>, {pep, NodeNS}, []),
  211:               pubsub_tools:receive_item_notification(
  212:                 Bob, <<"item1">>, {escalus_utils:get_short_jid(Alice), NodeNS}, [])
  213:       end).
  214: 
  215: set_caps_node(NodeNS, Config) ->
  216:     [{escalus_overrides,
  217:       [{initial_activity, {?MODULE, send_initial_presence_with_caps, [NodeNS]}}]}
  218:     | Config].
  219: 
  220: publish_options_test(Config) ->
  221:     % Given pubsub is configured with pep plugin
  222:     escalus:fresh_story(
  223:       Config,
  224:       [{alice, 1}],
  225:       fun(Alice) ->
  226: 
  227:             % When publishing an item with publish-options
  228:             Node = {pep, random_node_ns()},
  229:             PublishOptions = [{<<"pubsub#access_model">>, <<"open">>}],
  230:             pubsub_tools:publish_with_options(Alice, <<"item1">>, Node, [], PublishOptions),
  231: 
  232:             % Then node configuration contains specified publish-options
  233:             NodeConfig = pubsub_tools:get_configuration(Alice, Node, []),
  234:             verify_publish_options(NodeConfig, PublishOptions)
  235:       end).
  236: 
  237: send_caps_after_login_test(Config) ->
  238:     escalus:fresh_story(
  239:       Config,
  240:       [{alice, 1}, {bob, 1}],
  241:       fun(Alice, Bob) ->
  242:               NodeNS = random_node_ns(),
  243:               pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []),
  244: 
  245:               escalus_story:make_all_clients_friends([Alice, Bob]),
  246: 
  247:               %% Presence subscription triggers PEP last item sending
  248:               %% and sometimes this async process takes place after caps
  249:               %% are updated, leading to duplicated notification
  250:               %% We use timer:sleep here to avoid it for now, because
  251:               %% TODO: mod_pubsub send loop has to be fixed, supervised, refactored etc.
  252:               timer:sleep(1000),
  253: 
  254:               Caps = caps(NodeNS),
  255:               send_presence_with_caps_and_handle_disco(Bob, Caps, NodeNS),
  256:               receive_presence_with_caps(Bob, Bob, Caps),
  257:               receive_presence_with_caps(Alice, Bob, Caps),
  258: 
  259:               pubsub_tools:receive_item_notification(
  260:                 Bob, <<"item2">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []),
  261: 
  262:               [] = escalus_client:peek_stanzas(Bob)
  263:         end).
  264: 
  265: delayed_receive(Config) ->
  266: %%    if alice publishes an item and then bob subscribes successfully to her presence
  267: %%    then bob will receive the item right after final subscription stanzas
  268:     NodeNS = random_node_ns(),
  269:     escalus:fresh_story(
  270:         [{escalus_overrides,
  271:             [{initial_activity, {?MODULE, send_initial_presence_with_caps, [NodeNS]}}]}
  272:             | Config],
  273:         [{alice, 1}, {bob, 1}],
  274:         fun(Alice, Bob) ->
  275:             pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []),
  276:             [Message] = make_friends(Bob, Alice),
  277:             ct:pal("Message: ~p", [Message]),
  278:             pubsub_tools:check_item_notification(
  279:                 Message, <<"item2">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []),
  280:             ok
  281:         end).
  282: 
  283: delayed_receive_with_sm(Config) ->
  284: %%    Same as delayed_receive but with stream management turned on
  285:     NodeNS = random_node_ns(),
  286:     escalus:fresh_story(
  287:         [{escalus_overrides,
  288:             [{initial_activity, {?MODULE, send_initial_presence_with_caps, [NodeNS]}}]}
  289:             | Config],
  290:       [{alice, 1}, {bob, 1}],
  291:       fun(Alice, Bob) ->
  292:               enable_sm(Alice),
  293:               enable_sm(Bob),
  294:               publish_with_sm(Alice, <<"item2">>, {pep, NodeNS}, []),
  295:               [Message] = make_friends(Bob, Alice),
  296:               ct:pal("Message: ~p", [Message]),
  297:               pubsub_tools:check_item_notification(Message,
  298:                                                    <<"item2">>,
  299:                                                    {escalus_utils:get_short_jid(Alice), NodeNS},
  300:                                                    []),
  301:               ok
  302:       end).
  303: 
  304: h_ok_after_notify_test(ConfigIn) ->
  305:     Config = escalus_users:update_userspec(ConfigIn, kate,
  306:                                            stream_management, true),
  307:     NodeNS = random_node_ns(),
  308:     escalus:fresh_story(
  309:       [{escalus_overrides,
  310:         [{initial_activity, {?MODULE, send_initial_presence_with_caps, [NodeNS]}}]} | Config ],
  311:       [{alice, 1}, {kate, 1}],
  312:       fun(Alice, Kate) ->
  313:               escalus_story:make_all_clients_friends([Alice, Kate]),
  314: 
  315:               %% TODO: Dirty fix. For some reason PEP resends item2 with <delay> element,
  316:               %% so probably there is some race condition that applies to becoming friends
  317:               %% and publishing
  318:               timer:sleep(1000),
  319: 
  320:               pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []),
  321:               pubsub_tools:receive_item_notification(
  322:                 Kate, <<"item2">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []),
  323: 
  324:               H = escalus_tcp:get_sm_h(Kate#client.rcv_pid),
  325:               escalus:send(Kate, escalus_stanza:sm_ack(H)),
  326: 
  327:               escalus_connection:send(Kate, escalus_stanza:sm_request()),
  328:               escalus:assert(is_sm_ack,
  329:                              escalus_connection:get_stanza(Kate, stream_mgmt_ack))
  330:       end).
  331: 
  332: authorize_access_model(Config) ->
  333:     escalus:fresh_story(Config,
  334:       [{alice, 1}, {bob, 1}],
  335:       fun(Alice, Bob) ->
  336:               NodeNS = random_node_ns(),
  337:               {NodeAddr, _} = PepNode = make_pep_node_info(Alice, NodeNS),
  338:               AccessModel = {<<"pubsub#access_model">>, <<"authorize">>},
  339:               pubsub_tools:create_node(Alice, PepNode, [{config, [AccessModel]}]),
  340: 
  341:               pubsub_tools:subscribe(Bob, PepNode, [{subscription, <<"pending">>}]),
  342:               BobsRequest = pubsub_tools:receive_subscription_request(Alice, Bob, PepNode, []),
  343: 
  344:               %% FIXME: Only one item should be here but node_pep is based on node_flat, which
  345:               %% is node_dag's ancestor, so this entry gets duplicated because every plugin
  346:               %% is queried for subscriptions. Nasty fix involves deduplicating entries
  347:               %% in mod_pubsub:get_subscriptions. The proper fix means not hacking node plugins
  348:               %% into serving PEP but it's definitely a major change...
  349:               Subs = [{NodeNS, <<"pending">>}, {NodeNS, <<"pending">>}],
  350:               pubsub_tools:get_user_subscriptions(Bob, NodeAddr, [{expected_result, Subs}]),
  351: 
  352:               pubsub_tools:submit_subscription_response(Alice, BobsRequest, PepNode, true, []),
  353:               pubsub_tools:receive_subscription_notification(Bob, <<"subscribed">>, PepNode, []),
  354: 
  355:               pubsub_tools:publish(Alice, <<"fish">>, {pep, NodeNS}, []),
  356:               pubsub_tools:receive_item_notification(
  357:                 Bob, <<"fish">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []),
  358: 
  359:               pubsub_tools:delete_node(Alice, PepNode, [])
  360:       end).
  361: 
  362: unsubscribe_after_presence_unsubscription(Config) ->
  363:     escalus:fresh_story(Config,
  364:       [{alice, 1}, {bob, 1}],
  365:       fun(Alice, Bob) ->
  366:               escalus_story:make_all_clients_friends([Alice, Bob]),
  367: 
  368:               NodeNS = random_node_ns(),
  369:               PepNode = make_pep_node_info(Alice, NodeNS),
  370:               pubsub_tools:create_node(Alice, PepNode, []),
  371:               pubsub_tools:subscribe(Bob, PepNode, []),
  372:               pubsub_tools:publish(Alice, <<"fish">>, {pep, NodeNS}, []),
  373:               pubsub_tools:receive_item_notification(
  374:                 Bob, <<"fish">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []),
  375: 
  376:               BobJid = escalus_utils:get_short_jid(Bob),
  377:               escalus:send(Alice, escalus_stanza:presence_direct(BobJid, <<"unsubscribed">>)),
  378:               %% Bob & Alice get roster update, Bob gets presence unsubscribed & unavailable
  379:               [_, _, _] = escalus:wait_for_stanzas(Bob, 3),
  380:               _ = escalus:wait_for_stanza(Alice),
  381: 
  382:               %% Unsubscription from PEP nodes is implicit
  383:               pubsub_tools:publish(Alice, <<"salmon">>, {pep, NodeNS}, []),
  384:               [] = escalus:wait_for_stanzas(Bob, 1),
  385: 
  386:               pubsub_tools:delete_node(Alice, PepNode, [])
  387:       end).
  388: 
  389: %%-----------------------------------------------------------------
  390: %% Helpers
  391: %%-----------------------------------------------------------------
  392: 
  393: required_modules() ->
  394:     [{mod_caps, []},
  395:      {mod_pubsub, [
  396:                    {plugins, [<<"dag">>, <<"pep">>]},
  397:                    {nodetree, <<"dag">>},
  398:                    {backend, mongoose_helper:mnesia_or_rdbms_backend()},
  399:                    {pep_mapping, []},
  400:                    {host, subhost_pattern("pubsub.@HOST@")}
  401:                   ]}].
  402: required_modules(cache_tests) ->
  403:     [{mod_caps, []},
  404:      {mod_pubsub, [
  405:                    {plugins, [<<"dag">>, <<"pep">>]},
  406:                    {nodetree, <<"dag">>},
  407:                    {backend, mongoose_helper:mnesia_or_rdbms_backend()},
  408:                    {pep_mapping, []},
  409:                    {host, subhost_pattern("pubsub.@HOST@")},
  410:                    {last_item_cache, mongoose_helper:mnesia_or_rdbms_backend()}
  411:                   ]}].
  412: 
  413: send_initial_presence_with_caps(NodeNS, User) ->
  414:     case string:to_lower(binary_to_list(escalus_client:username(User))) of
  415:         "alice" ++ _ -> escalus_story:send_initial_presence(User);
  416:         "bob" ++ _ -> send_presence_with_caps_and_handle_disco(User, caps(NodeNS), NodeNS);
  417:         "kate" ++ _ -> send_presence_with_caps_and_handle_disco(User, caps(NodeNS), NodeNS)
  418:     end.
  419: 
  420: send_presence_with_caps_and_handle_disco(User, Caps, NodeNS) ->
  421:     send_presence_with_caps(User, Caps),
  422:     DiscoRequest = escalus:wait_for_stanza(User),
  423:     send_caps_disco_result(User, DiscoRequest, NodeNS).
  424: 
  425: send_presence_with_caps(User, Caps) ->
  426:     Presence = escalus_stanza:presence(<<"available">>, [Caps]),
  427:     escalus:send(User, Presence).
  428: 
  429: send_caps_disco_result(User, DiscoRequest, NodeNS) ->
  430:     QueryEl = escalus_stanza:query_el(?NS_DISCO_INFO, feature_elems(NodeNS)),
  431:     DiscoResult = escalus_stanza:iq_result(DiscoRequest, [QueryEl]),
  432:     escalus:send(User, DiscoResult).
  433: 
  434: receive_presence_with_caps(User1, User2, Caps) ->
  435:     PresenceNotification = escalus:wait_for_stanza(User1),
  436:     escalus:assert(is_presence, PresenceNotification),
  437:     escalus:assert(is_stanza_from, [User2], PresenceNotification),
  438:     Caps = exml_query:subelement(PresenceNotification, <<"c">>).
  439: 
  440: make_pep_node_info(Client, NodeName) ->
  441:     {escalus_utils:jid_to_lower(escalus_utils:get_short_jid(Client)), NodeName}.
  442: 
  443: verify_publish_options(FullNodeConfig, Options) ->
  444:     NodeConfig = [{Opt, Value} || {Opt, _, Value} <- FullNodeConfig],
  445:     Options = lists:filter(fun(Option) ->
  446:                                lists:member(Option, NodeConfig)
  447:                            end, Options).
  448: 
  449: %%-----------------------------------------------------------------
  450: %% XML helpers
  451: %%-----------------------------------------------------------------
  452: 
  453: feature_elems(PEPNodeNS) ->
  454:     [#xmlel{name = <<"identity">>,
  455:             attrs = [{<<"category">>, <<"client">>},
  456:                      {<<"name">>, <<"Psi">>},
  457:                      {<<"type">>, <<"pc">>}]} |
  458:      [feature_elem(F) || F <- features(PEPNodeNS)]].
  459: 
  460: feature_elem(F) ->
  461:     #xmlel{name = <<"feature">>,
  462:            attrs = [{<<"var">>, F}]}.
  463: 
  464: caps(PEPNodeNS) ->
  465:     #xmlel{name = <<"c">>,
  466:            attrs = [{<<"xmlns">>, ?NS_CAPS},
  467:                     {<<"hash">>, <<"sha-1">>},
  468:                     {<<"node">>, caps_node_name()},
  469:                     {<<"ver">>, caps_hash(PEPNodeNS)}]}.
  470: 
  471: features(PEPNodeNS) ->
  472:     [?NS_DISCO_INFO,
  473:      ?NS_DISCO_ITEMS,
  474:      ?NS_GEOLOC,
  475:      ns_notify(?NS_GEOLOC),
  476:      PEPNodeNS,
  477:      ns_notify(PEPNodeNS)].
  478: 
  479: ns_notify(NS) ->
  480:     <<NS/binary, "+notify">>.
  481: 
  482: random_node_ns() ->
  483:     base64:encode(crypto:strong_rand_bytes(16)).
  484: 
  485: caps_hash(PEPNodeNS) ->
  486:     rpc(mim(), mod_caps, make_disco_hash, [feature_elems(PEPNodeNS), sha1]).
  487: 
  488: caps_node_name() ->
  489:     <<"http://www.chatopus.com">>.
  490: 
  491: send_presence(From, Type, To) ->
  492:     ToJid = escalus_client:short_jid(To),
  493:     Stanza = escalus_stanza:presence_direct(ToJid, Type),
  494:     escalus_client:send(From, Stanza).
  495: 
  496: make_friends(Bob, Alice) ->
  497:     % makes uni-directional presence subscriptions
  498:     % returns stanzas received finally by the inviter
  499:     send_presence(Bob, <<"subscribe">>, Alice),
  500:     send_presence(Alice, <<"subscribed">>, Bob),
  501:     escalus:wait_for_stanzas(Alice, 10, 200),
  502:     BobStanzas = escalus:wait_for_stanzas(Bob, 10, 200),
  503:     lists:filter(fun(S) -> N = S#xmlel.name,
  504:                            N =/= <<"iq">>
  505:                            andalso
  506:                            N =/= <<"presence">>
  507:                            andalso
  508:                            N =/= <<"r">>
  509:                  end,
  510:                  BobStanzas).
  511: 
  512: publish_with_sm(User, ItemId, Node, Options) ->
  513:     Id = id(User, Node, <<"publish">>),
  514:     Request = case proplists:get_value(with_payload, Options, true) of
  515:                   true -> escalus_pubsub_stanza:publish(User, ItemId, item_content(), Id, Node);
  516:                   false -> escalus_pubsub_stanza:publish(User, Id, Node)
  517:               end,
  518:     escalus_client:send(User, Request),
  519:     escalus:wait_for_stanzas(User, 2).
  520: 
  521: id(User, {NodeAddr, NodeName}, Suffix) ->
  522:     UserName = escalus_utils:get_username(User),
  523:     list_to_binary(io_lib:format("~s-~s-~s-~s", [UserName, NodeAddr, NodeName, Suffix])).
  524: 
  525: item_content() ->
  526:     #xmlel{name = <<"entry">>,
  527:         attrs = [{<<"xmlns">>, <<"http://www.w3.org/2005/Atom">>}]}.
  528: 
  529: enable_sm(User) ->
  530:     escalus_client:send(User, escalus_stanza:enable_sm()),
  531:     #xmlel{name = <<"enabled">>} = escalus:wait_for_stanza(User).