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: -import(config_parser_helper, [mod_config/2]).
   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, config_parser_helper:default_mod_config(mod_caps)},
  395:      {mod_pubsub, mod_config(mod_pubsub, #{plugins => [<<"dag">>, <<"pep">>],
  396:                                            nodetree => nodetree_dag,
  397:                                            backend => mongoose_helper:mnesia_or_rdbms_backend(),
  398:                                            pep_mapping => #{},
  399:                                            host => subhost_pattern("pubsub.@HOST@")})}].
  400: required_modules(cache_tests) ->
  401:     [{mod_caps, config_parser_helper:default_mod_config(mod_caps)},
  402:      {mod_pubsub, mod_config(mod_pubsub, #{plugins => [<<"dag">>, <<"pep">>],
  403:                                            nodetree => nodetree_dag,
  404:                                            backend => mongoose_helper:mnesia_or_rdbms_backend(),
  405:                                            pep_mapping => #{},
  406:                                            host => subhost_pattern("pubsub.@HOST@"),
  407:                                            last_item_cache => mongoose_helper:mnesia_or_rdbms_backend()
  408:      })}].
  409: 
  410: send_initial_presence_with_caps(NodeNS, User) ->
  411:     case string:to_lower(binary_to_list(escalus_client:username(User))) of
  412:         "alice" ++ _ -> escalus_story:send_initial_presence(User);
  413:         "bob" ++ _ -> send_presence_with_caps_and_handle_disco(User, caps(NodeNS), NodeNS);
  414:         "kate" ++ _ -> send_presence_with_caps_and_handle_disco(User, caps(NodeNS), NodeNS)
  415:     end.
  416: 
  417: send_presence_with_caps_and_handle_disco(User, Caps, NodeNS) ->
  418:     send_presence_with_caps(User, Caps),
  419:     DiscoRequest = escalus:wait_for_stanza(User),
  420:     send_caps_disco_result(User, DiscoRequest, NodeNS).
  421: 
  422: send_presence_with_caps(User, Caps) ->
  423:     Presence = escalus_stanza:presence(<<"available">>, [Caps]),
  424:     escalus:send(User, Presence).
  425: 
  426: send_caps_disco_result(User, DiscoRequest, NodeNS) ->
  427:     QueryEl = escalus_stanza:query_el(?NS_DISCO_INFO, feature_elems(NodeNS)),
  428:     DiscoResult = escalus_stanza:iq_result(DiscoRequest, [QueryEl]),
  429:     escalus:send(User, DiscoResult).
  430: 
  431: receive_presence_with_caps(User1, User2, Caps) ->
  432:     PresenceNotification = escalus:wait_for_stanza(User1),
  433:     escalus:assert(is_presence, PresenceNotification),
  434:     escalus:assert(is_stanza_from, [User2], PresenceNotification),
  435:     Caps = exml_query:subelement(PresenceNotification, <<"c">>).
  436: 
  437: make_pep_node_info(Client, NodeName) ->
  438:     {escalus_utils:jid_to_lower(escalus_utils:get_short_jid(Client)), NodeName}.
  439: 
  440: verify_publish_options(FullNodeConfig, Options) ->
  441:     NodeConfig = [{Opt, Value} || {Opt, _, Value} <- FullNodeConfig],
  442:     Options = lists:filter(fun(Option) ->
  443:                                lists:member(Option, NodeConfig)
  444:                            end, Options).
  445: 
  446: %%-----------------------------------------------------------------
  447: %% XML helpers
  448: %%-----------------------------------------------------------------
  449: 
  450: feature_elems(PEPNodeNS) ->
  451:     [#xmlel{name = <<"identity">>,
  452:             attrs = [{<<"category">>, <<"client">>},
  453:                      {<<"name">>, <<"Psi">>},
  454:                      {<<"type">>, <<"pc">>}]} |
  455:      [feature_elem(F) || F <- features(PEPNodeNS)]].
  456: 
  457: feature_elem(F) ->
  458:     #xmlel{name = <<"feature">>,
  459:            attrs = [{<<"var">>, F}]}.
  460: 
  461: caps(PEPNodeNS) ->
  462:     #xmlel{name = <<"c">>,
  463:            attrs = [{<<"xmlns">>, ?NS_CAPS},
  464:                     {<<"hash">>, <<"sha-1">>},
  465:                     {<<"node">>, caps_node_name()},
  466:                     {<<"ver">>, caps_hash(PEPNodeNS)}]}.
  467: 
  468: features(PEPNodeNS) ->
  469:     [?NS_DISCO_INFO,
  470:      ?NS_DISCO_ITEMS,
  471:      ?NS_GEOLOC,
  472:      ns_notify(?NS_GEOLOC),
  473:      PEPNodeNS,
  474:      ns_notify(PEPNodeNS)].
  475: 
  476: ns_notify(NS) ->
  477:     <<NS/binary, "+notify">>.
  478: 
  479: random_node_ns() ->
  480:     base64:encode(crypto:strong_rand_bytes(16)).
  481: 
  482: caps_hash(PEPNodeNS) ->
  483:     rpc(mim(), mod_caps, make_disco_hash, [feature_elems(PEPNodeNS), sha1]).
  484: 
  485: caps_node_name() ->
  486:     <<"http://www.chatopus.com">>.
  487: 
  488: send_presence(From, Type, To) ->
  489:     ToJid = escalus_client:short_jid(To),
  490:     Stanza = escalus_stanza:presence_direct(ToJid, Type),
  491:     escalus_client:send(From, Stanza).
  492: 
  493: make_friends(Bob, Alice) ->
  494:     % makes uni-directional presence subscriptions
  495:     % returns stanzas received finally by the inviter
  496:     send_presence(Bob, <<"subscribe">>, Alice),
  497:     send_presence(Alice, <<"subscribed">>, Bob),
  498:     escalus:wait_for_stanzas(Alice, 10, 200),
  499:     BobStanzas = escalus:wait_for_stanzas(Bob, 10, 200),
  500:     lists:filter(fun(S) -> N = S#xmlel.name,
  501:                            N =/= <<"iq">>
  502:                            andalso
  503:                            N =/= <<"presence">>
  504:                            andalso
  505:                            N =/= <<"r">>
  506:                  end,
  507:                  BobStanzas).
  508: 
  509: publish_with_sm(User, ItemId, Node, Options) ->
  510:     Id = id(User, Node, <<"publish">>),
  511:     Request = case proplists:get_value(with_payload, Options, true) of
  512:                   true -> escalus_pubsub_stanza:publish(User, ItemId, item_content(), Id, Node);
  513:                   false -> escalus_pubsub_stanza:publish(User, Id, Node)
  514:               end,
  515:     escalus_client:send(User, Request),
  516:     escalus:wait_for_stanzas(User, 2).
  517: 
  518: id(User, {NodeAddr, NodeName}, Suffix) ->
  519:     UserName = escalus_utils:get_username(User),
  520:     list_to_binary(io_lib:format("~s-~s-~s-~s", [UserName, NodeAddr, NodeName, Suffix])).
  521: 
  522: item_content() ->
  523:     #xmlel{name = <<"entry">>,
  524:         attrs = [{<<"xmlns">>, <<"http://www.w3.org/2005/Atom">>}]}.
  525: 
  526: enable_sm(User) ->
  527:     escalus_client:send(User, escalus_stanza:enable_sm()),
  528:     #xmlel{name = <<"enabled">>} = escalus:wait_for_stanza(User).