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("eunit/include/eunit.hrl").
   15: 
   16: -export([suite/0, all/0, groups/0]).
   17: -export([init_per_suite/1, end_per_suite/1,
   18:          init_per_group/2, end_per_group/2,
   19:          init_per_testcase/2, end_per_testcase/2]).
   20: 
   21: -export([
   22:          disco_test/1,
   23:          disco_sm_test/1,
   24:          disco_sm_items_test/1,
   25:          pep_caps_test/1,
   26:          publish_and_notify_test/1,
   27:          auto_create_with_publish_options_test/1,
   28:          publish_options_success_test/1,
   29:          publish_options_fail_unknown_option_story/1,
   30:          publish_options_fail_wrong_value_story/1,
   31:          publish_options_fail_wrong_form/1,
   32:          send_caps_after_login_test/1,
   33:          delayed_receive/1,
   34:          delayed_receive_with_sm/1,
   35:          h_ok_after_notify_test/1,
   36:          authorize_access_model/1,
   37:          unsubscribe_after_presence_unsubscription/1
   38:         ]).
   39: 
   40: -export([
   41:          start_caps_clients/2,
   42:          send_initial_presence_with_caps/2,
   43:          add_config_to_create_node_request/1
   44:         ]).
   45: 
   46: -import(distributed_helper, [mim/0,
   47:                              require_rpc_nodes/1,
   48:                              subhost_pattern/1,
   49:                              rpc/4]).
   50: -import(config_parser_helper, [mod_config/2]).
   51: -import(domain_helper, [domain/0]).
   52: 
   53: -define(NS_PUBSUB_PUB_OPTIONS,  <<"http://jabber.org/protocol/pubsub#publish-options">>).
   54: 
   55: %%--------------------------------------------------------------------
   56: %% Suite configuration
   57: %%--------------------------------------------------------------------
   58: 
   59: all() ->
   60:     [
   61:      {group, pep_tests},
   62:      {group, cache_tests}
   63:     ].
   64: 
   65: groups() ->
   66:     [
   67:          {pep_tests, [parallel],
   68:           [
   69:            disco_test,
   70:            disco_sm_test,
   71:            disco_sm_items_test,
   72:            pep_caps_test,
   73:            publish_and_notify_test,
   74:            auto_create_with_publish_options_test,
   75:            publish_options_success_test,
   76:            publish_options_fail_unknown_option_story,
   77:            publish_options_fail_wrong_value_story,
   78:            publish_options_fail_wrong_form,
   79:            send_caps_after_login_test,
   80:            delayed_receive,
   81:            delayed_receive_with_sm,
   82:            h_ok_after_notify_test,
   83:            authorize_access_model,
   84:            unsubscribe_after_presence_unsubscription
   85:           ]
   86:          },
   87:          {cache_tests, [parallel],
   88:           [
   89:            send_caps_after_login_test,
   90:            delayed_receive,
   91:            delayed_receive_with_sm,
   92:            unsubscribe_after_presence_unsubscription
   93:           ]
   94:          }
   95:     ].
   96: 
   97: suite() ->
   98:     require_rpc_nodes([mim]) ++ escalus:suite().
   99: 
  100: %%--------------------------------------------------------------------
  101: %% Init & teardown
  102: %%--------------------------------------------------------------------
  103: 
  104: init_per_suite(Config) ->
  105:     escalus:init_per_suite(dynamic_modules:save_modules(domain(), Config)).
  106: 
  107: end_per_suite(Config) ->
  108:     escalus_fresh:clean(),
  109:     dynamic_modules:restore_modules(Config),
  110:     escalus:end_per_suite(Config).
  111: 
  112: init_per_group(cache_tests, Config) ->
  113:     Config0 = dynamic_modules:save_modules(domain(), Config),
  114:     NewConfig = required_modules(cache_tests),
  115:     dynamic_modules:ensure_modules(domain(), NewConfig),
  116:     Config0;
  117: 
  118: init_per_group(_GroupName, Config) ->
  119:     dynamic_modules:ensure_modules(domain(), required_modules()),
  120:     Config.
  121: 
  122: end_per_group(_GroupName, Config) ->
  123:     dynamic_modules:restore_modules(Config).
  124: 
  125: init_per_testcase(TestName, Config) ->
  126:     escalus:init_per_testcase(TestName, Config).
  127: 
  128: end_per_testcase(TestName, Config) ->
  129:     escalus:end_per_testcase(TestName, Config).
  130: 
  131: %%--------------------------------------------------------------------
  132: %% Test cases for XEP-0163
  133: %% Comments in test cases refer to sections is the XEP
  134: %%--------------------------------------------------------------------
  135: 
  136: %% Group: pep_tests (sequence)
  137: 
  138: disco_test(Config) ->
  139:     escalus:fresh_story(
  140:       Config,
  141:       [{alice, 1}],
  142:       fun(Alice) ->
  143:               escalus:send(Alice, escalus_stanza:disco_info(pubsub_tools:node_addr())),
  144:               Stanza = escalus:wait_for_stanza(Alice),
  145:               escalus:assert(has_identity, [<<"pubsub">>, <<"service">>], Stanza),
  146:               escalus:assert(has_identity, [<<"pubsub">>, <<"pep">>], Stanza),
  147:               escalus:assert(has_feature, [?NS_PUBSUB], Stanza)
  148:       end).
  149: 
  150: disco_sm_test(Config) ->
  151:     escalus:fresh_story(
  152:       Config,
  153:       [{alice, 1}],
  154:       fun(Alice) ->
  155:               AliceJid = escalus_client:short_jid(Alice),
  156:               escalus:send(Alice, escalus_stanza:disco_info(AliceJid)),
  157:               Stanza = escalus:wait_for_stanza(Alice),
  158:               ?assertNot(escalus_pred:has_identity(<<"pubsub">>, <<"service">>, Stanza)),
  159:               escalus:assert(has_identity, [<<"pubsub">>, <<"pep">>], Stanza),
  160:               escalus:assert(has_feature, [?NS_PUBSUB], Stanza),
  161:               escalus:assert(is_stanza_from, [AliceJid], Stanza)
  162:       end).
  163: 
  164: disco_sm_items_test(Config) ->
  165:     NodeNS = random_node_ns(),
  166:     escalus:fresh_story(
  167:       Config,
  168:       [{alice, 1}],
  169:       fun(Alice) ->
  170:               AliceJid = escalus_client:short_jid(Alice),
  171: 
  172:               %% Node not present yet
  173:               escalus:send(Alice, escalus_stanza:disco_items(AliceJid)),
  174:               Stanza1 = escalus:wait_for_stanza(Alice),
  175:               Query1 = exml_query:subelement(Stanza1, <<"query">>),
  176:               ?assertEqual(undefined, exml_query:subelement_with_attr(Query1, <<"node">>, NodeNS)),
  177:               escalus:assert(is_stanza_from, [AliceJid], Stanza1),
  178: 
  179:               %% Publish an item to trigger node creation
  180:               pubsub_tools:publish(Alice, <<"item1">>, {pep, NodeNS}, []),
  181: 
  182:               %% Node present
  183:               escalus:send(Alice, escalus_stanza:disco_items(AliceJid)),
  184:               Stanza2 = escalus:wait_for_stanza(Alice),
  185:               Query2 = exml_query:subelement(Stanza2, <<"query">>),
  186:               Item = exml_query:subelement_with_attr(Query2, <<"node">>, NodeNS),
  187:               ?assertEqual(jid:str_tolower(AliceJid), exml_query:attr(Item, <<"jid">>)),
  188:               escalus:assert(is_stanza_from, [AliceJid], Stanza2)
  189:       end).
  190: 
  191: pep_caps_test(Config) ->
  192:     escalus:fresh_story(
  193:       Config,
  194:       [{bob, 1}],
  195:       fun(Bob) ->
  196:               NodeNS = random_node_ns(),
  197:               Caps = caps(NodeNS),
  198: 
  199:               %% Send presence with capabilities (chap. 1 ex. 4)
  200:               send_presence_with_caps(Bob, Caps),
  201:               receive_presence_with_caps(Bob, Bob, Caps),
  202: 
  203:               %% Server does not know the version string, so it requests feature list
  204:               DiscoRequest = escalus:wait_for_stanza(Bob),
  205:               %% Client responds with a list of supported features (chap. 1 ex. 5)
  206:               send_caps_disco_result(Bob, DiscoRequest, NodeNS)
  207:       end).
  208: 
  209: publish_and_notify_test(Config) ->
  210:     Config1 = set_caps(Config),
  211:     escalus:fresh_story_with_config(Config1, [{alice, 1}, {bob, 1}], fun publish_and_notify_story/3).
  212: 
  213: publish_and_notify_story(Config, Alice, Bob) ->
  214:     NodeNS = ?config(node_ns, Config),
  215:     escalus_story:make_all_clients_friends([Alice, Bob]),
  216:     pubsub_tools:publish(Alice, <<"item1">>, {pep, NodeNS}, []),
  217:     pubsub_tools:receive_item_notification(
  218:       Bob, <<"item1">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []).
  219: 
  220: auto_create_with_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: publish_options_success_test(Config) ->
  238:     escalus:fresh_story(
  239:       Config,
  240:       [{alice, 1}, {bob, 1}],
  241:       fun(Alice, Bob) ->
  242:             NodeNS = random_node_ns(),
  243:             PepNode = make_pep_node_info(Alice, NodeNS),
  244:             pubsub_tools:create_node(Alice, PepNode,
  245:                                      [{modify_request,fun add_config_to_create_node_request/1}]),
  246:             escalus_story:make_all_clients_friends([Alice, Bob]),
  247:             PublishOptions = [{<<"pubsub#deliver_payloads">>, <<"1">>},
  248:                               {<<"pubsub#notify_config">>, <<"0">>},
  249:                               {<<"pubsub#notify_delete">>, <<"0">>},
  250:                               {<<"pubsub#purge_offline">>, <<"0">>},
  251:                               {<<"pubsub#notify_retract">>, <<"0">>},
  252:                               {<<"pubsub#persist_items">>, <<"1">>},
  253:                               {<<"pubsub#roster_groups_allowed">>, [<<"friends">>, <<"enemies">>]},
  254:                               {<<"pubsub#max_items">>, <<"1">>},
  255:                               {<<"pubsub#subscribe">>, <<"1">>},
  256:                               {<<"pubsub#access_model">>, <<"presence">>},
  257:                               {<<"pubsub#publish_model">>, <<"publishers">>},
  258:                               {<<"pubsub#notification_type">>, <<"headline">>},
  259:                               {<<"pubsub#max_payload_size">>, <<"60000">>},
  260:                               {<<"pubsub#send_last_published_item">>, <<"on_sub_and_presence">>},
  261:                               {<<"pubsub#deliver_notifications">>, <<"1">>},
  262:                               {<<"pubsub#presence_based_delivery">>, <<"1">>}],
  263:             Result = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions),
  264:             escalus:assert(is_iq_result, Result)
  265:       end).
  266: 
  267: publish_options_fail_unknown_option_story(Config) ->
  268:     escalus:fresh_story(
  269:       Config,
  270:       [{alice, 1}],
  271:       fun(Alice) ->
  272:             NodeNS = random_node_ns(),
  273:             PepNode = make_pep_node_info(Alice, NodeNS),
  274:             pubsub_tools:create_node(Alice, PepNode, []),
  275: 
  276:             PublishOptions = [{<<"deliver_payloads">>, <<"1">>}],
  277:             Result = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions),
  278:             escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result),
  279: 
  280:             PublishOptions2 = [{<<"pubsub#not_existing_option">>, <<"1">>}],
  281:             Result2 = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions2),
  282:             escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result2)
  283:       end).
  284: 
  285: publish_options_fail_wrong_value_story(Config) ->
  286:     escalus:fresh_story(
  287:       Config,
  288:       [{alice, 1}],
  289:       fun(Alice) ->
  290:             NodeNS = random_node_ns(),
  291:             PepNode = make_pep_node_info(Alice, NodeNS),
  292:             pubsub_tools:create_node(Alice, PepNode,
  293:                                      [{modify_request,fun add_config_to_create_node_request/1}]),
  294: 
  295:             PublishOptions = [{<<"pubsub#deliver_payloads">>, <<"0">>}],
  296:             Result = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions),
  297:             escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result),
  298: 
  299:             PublishOptions2 = [{<<"pubsub#roster_groups_allowed">>, <<"friends">>}],
  300:             Result2 = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions2),
  301:             escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result2)
  302:       end).
  303: 
  304: publish_options_fail_wrong_form(Config) ->
  305:     escalus:fresh_story(
  306:       Config,
  307:       [{alice, 1}],
  308:       fun(Alice) ->
  309:             NodeNS = random_node_ns(),
  310:             PepNode = make_pep_node_info(Alice, NodeNS),
  311:             pubsub_tools:create_node(Alice, PepNode, []),
  312:             PublishOptions = [{<<"deliver_payloads">>, <<"0">>}],
  313:             Result = publish_with_publish_options(Alice, {pep, NodeNS}, <<"item1">>, PublishOptions, <<"WRONG_NS">>),
  314:             escalus:assert(is_error, [<<"cancel">>, <<"conflict">>], Result)
  315:       end).
  316: 
  317: send_caps_after_login_test(Config) ->
  318:     escalus:fresh_story(
  319:       Config,
  320:       [{alice, 1}, {bob, 1}],
  321:       fun(Alice, Bob) ->
  322:               NodeNS = random_node_ns(),
  323:               pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []),
  324: 
  325:               escalus_story:make_all_clients_friends([Alice, Bob]),
  326: 
  327:               Caps = caps(NodeNS),
  328:               send_presence_with_caps(Bob, Caps),
  329:               receive_presence_with_caps(Bob, Bob, Caps),
  330:               receive_presence_with_caps(Alice, Bob, Caps),
  331: 
  332:               handle_requested_caps(NodeNS, Bob),
  333: 
  334:               Node = {escalus_utils:get_short_jid(Alice), NodeNS},
  335:               Check = fun(Message) ->
  336:                               pubsub_tools:check_item_notification(Message, <<"item2">>, Node, [])
  337:                       end,
  338: 
  339:               %% Presence subscription triggers PEP last item sending
  340:               %% and sometimes this async process takes place after caps
  341:               %% are updated, leading to duplicated notification
  342:               Check(escalus_client:wait_for_stanza(Bob)),
  343:               case escalus_client:peek_stanzas(Bob) of
  344:                   [Message2] ->
  345:                       Check(Message2);
  346:                   [] ->
  347:                       ok
  348:               end
  349:         end).
  350: 
  351: delayed_receive(Config) ->
  352:     %% if alice publishes an item and then bob subscribes successfully to her presence
  353:     %% then bob will receive the item right after final subscription stanzas
  354:     Config1 = set_caps(Config),
  355:     escalus:fresh_story_with_config(Config1, [{alice, 1}, {bob, 1}], fun delayed_receive_story/3).
  356: 
  357: delayed_receive_story(Config, Alice, Bob) ->
  358:     NodeNS = ?config(node_ns, Config),
  359:     pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []),
  360:     [Message] = make_friends(Bob, Alice),
  361:     Node = {escalus_utils:get_short_jid(Alice), NodeNS},
  362:     pubsub_tools:check_item_notification(Message, <<"item2">>, Node, []),
  363: 
  364:     %% Known issue: without a mutual presence subscription Bob will not receive further items
  365:     pubsub_tools:publish(Alice, <<"item3">>, {pep, NodeNS}, []),
  366:     [] = escalus_client:wait_for_stanzas(Bob, 1, 500),
  367:     ok.
  368: 
  369: delayed_receive_with_sm(Config) ->
  370:     %% Same as delayed_receive but with stream management turned on
  371:     Config1 = set_caps(Config),
  372:     escalus:fresh_story_with_config(Config1, [{alice, 1}, {bob, 1}],
  373:                                     fun delayed_receive_with_sm_story/3).
  374: 
  375: delayed_receive_with_sm_story(Config, Alice, Bob) ->
  376:     NodeNS = ?config(node_ns, Config),
  377:     enable_sm(Alice),
  378:     enable_sm(Bob),
  379:     publish_with_sm(Alice, <<"item2">>, {pep, NodeNS}, []),
  380:     [Message] = make_friends_sm(Bob, Alice),
  381:     Node = {escalus_utils:get_short_jid(Alice), NodeNS},
  382:     pubsub_tools:check_item_notification(Message, <<"item2">>, Node, []).
  383: 
  384: h_ok_after_notify_test(ConfigIn) ->
  385:     Config = escalus_users:update_userspec(ConfigIn, kate, stream_management, true),
  386:     Config1 = set_caps(Config),
  387:     escalus:fresh_story_with_config(Config1, [{alice, 1}, {kate, 1}],
  388:                                     fun h_ok_after_notify_story/3).
  389: 
  390: h_ok_after_notify_story(Config, Alice, Kate) ->
  391:     NodeNS = ?config(node_ns, Config),
  392:     escalus_story:make_all_clients_friends([Alice, Kate]),
  393: 
  394:     pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []),
  395:     Node = {escalus_utils:get_short_jid(Alice), NodeNS},
  396:     Check = fun(Message) ->
  397:                     pubsub_tools:check_item_notification(Message, <<"item2">>, Node, [])
  398:             end,
  399:     Check(escalus_connection:get_stanza(Kate, item2)),
  400: 
  401:     H = escalus_tcp:get_sm_h(Kate#client.rcv_pid),
  402:     escalus:send(Kate, escalus_stanza:sm_ack(H)),
  403: 
  404:     escalus_connection:send(Kate, escalus_stanza:sm_request()),
  405: 
  406:     % Presence exchange triggers asynchronous sending of the last published item.
  407:     % If this happens after item2 is published, Kate will receive it twice.
  408:     Stanza = escalus_connection:get_stanza(Kate, stream_mgmt_ack_or_item2),
  409:     case escalus_pred:is_sm_ack(Stanza) of
  410:         true ->
  411:             ok;
  412:         false ->
  413:             Check(Stanza),
  414:             escalus:assert(is_sm_ack, escalus_connection:get_stanza(Kate, stream_mgmt_ack))
  415:     end.
  416: 
  417: authorize_access_model(Config) ->
  418:     escalus:fresh_story(Config,
  419:       [{alice, 1}, {bob, 1}],
  420:       fun(Alice, Bob) ->
  421:               NodeNS = random_node_ns(),
  422:               {NodeAddr, _} = PepNode = make_pep_node_info(Alice, NodeNS),
  423:               AccessModel = {<<"pubsub#access_model">>, <<"authorize">>},
  424:               pubsub_tools:create_node(Alice, PepNode, [{config, [AccessModel]}]),
  425: 
  426:               pubsub_tools:subscribe(Bob, PepNode, [{subscription, <<"pending">>}]),
  427:               BobsRequest = pubsub_tools:receive_subscription_request(Alice, Bob, PepNode, []),
  428: 
  429:               %% FIXME: Only one item should be here but node_pep is based on node_flat, which
  430:               %% is node_dag's ancestor, so this entry gets duplicated because every plugin
  431:               %% is queried for subscriptions. Nasty fix involves deduplicating entries
  432:               %% in mod_pubsub:get_subscriptions. The proper fix means not hacking node plugins
  433:               %% into serving PEP but it's definitely a major change...
  434:               Subs = [{NodeNS, <<"pending">>}, {NodeNS, <<"pending">>}],
  435:               pubsub_tools:get_user_subscriptions(Bob, NodeAddr, [{expected_result, Subs}]),
  436: 
  437:               pubsub_tools:submit_subscription_response(Alice, BobsRequest, PepNode, true, []),
  438:               pubsub_tools:receive_subscription_notification(Bob, <<"subscribed">>, PepNode, []),
  439: 
  440:               pubsub_tools:publish(Alice, <<"fish">>, {pep, NodeNS}, []),
  441:               pubsub_tools:receive_item_notification(
  442:                 Bob, <<"fish">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []),
  443: 
  444:               pubsub_tools:delete_node(Alice, PepNode, [])
  445:       end).
  446: 
  447: unsubscribe_after_presence_unsubscription(Config) ->
  448:     escalus:fresh_story(Config,
  449:       [{alice, 1}, {bob, 1}],
  450:       fun(Alice, Bob) ->
  451:               escalus_story:make_all_clients_friends([Alice, Bob]),
  452: 
  453:               NodeNS = random_node_ns(),
  454:               PepNode = make_pep_node_info(Alice, NodeNS),
  455:               pubsub_tools:create_node(Alice, PepNode, []),
  456:               pubsub_tools:subscribe(Bob, PepNode, []),
  457:               pubsub_tools:publish(Alice, <<"fish">>, {pep, NodeNS}, []),
  458:               pubsub_tools:receive_item_notification(
  459:                 Bob, <<"fish">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []),
  460: 
  461:               BobJid = escalus_utils:get_short_jid(Bob),
  462:               escalus:send(Alice, escalus_stanza:presence_direct(BobJid, <<"unsubscribed">>)),
  463:               %% Bob & Alice get roster update, Bob gets presence unsubscribed & unavailable
  464:               [_, _, _] = escalus:wait_for_stanzas(Bob, 3),
  465:               _ = escalus:wait_for_stanza(Alice),
  466: 
  467:               %% Unsubscription from PEP nodes is implicit
  468:               pubsub_tools:publish(Alice, <<"salmon">>, {pep, NodeNS}, []),
  469:               [] = escalus:wait_for_stanzas(Bob, 1, 500),
  470: 
  471:               pubsub_tools:delete_node(Alice, PepNode, [])
  472:       end).
  473: 
  474: %%-----------------------------------------------------------------
  475: %% Helpers
  476: %%-----------------------------------------------------------------
  477: 
  478: add_config_to_create_node_request(#xmlel{children = [PubsubEl]} = Request) ->
  479:     Fields = [#{values => [<<"friends">>, <<"enemies">>], var => <<"pubsub#roster_groups_allowed">>}],
  480:     Form = form_helper:form(#{ns => <<"http://jabber.org/protocol/pubsub#node_config">>, fields => Fields}),
  481:     ConfigureEl = #xmlel{name = <<"configure">>, children = [Form]},
  482:     PubsubEl2 = PubsubEl#xmlel{children = PubsubEl#xmlel.children ++ [ConfigureEl]},
  483:     Request#xmlel{children = [PubsubEl2]}.
  484:  
  485: publish_with_publish_options(Client, Node, Content, Options) ->
  486:     publish_with_publish_options(Client, Node, Content, Options, ?NS_PUBSUB_PUB_OPTIONS).
  487: 
  488: publish_with_publish_options(Client, Node, Content, Options, FormType) ->
  489:     OptionsEl = #xmlel{name = <<"publish-options">>,
  490:                        children = form(Options, FormType)},
  491: 
  492:     Id = pubsub_tools:id(Client, Node, <<"publish">>),
  493:     Publish = pubsub_tools:publish_request(Id, Client, Content, Node, Options),
  494:     #xmlel{children = [#xmlel{} = PubsubEl]} = Publish,
  495:     NewPubsubEl = PubsubEl#xmlel{children = PubsubEl#xmlel.children ++ [OptionsEl]},
  496:     escalus:send(Client, Publish#xmlel{children = [NewPubsubEl]}),
  497:     escalus:wait_for_stanza(Client).
  498: 
  499: form(FormFields, FormType) ->
  500:     FieldSpecs = lists:map(fun field_spec/1, FormFields),
  501:     [form_helper:form(#{fields => FieldSpecs, ns => FormType})].
  502: 
  503: field_spec({Var, Value}) when is_list(Value) -> #{var => Var, values => Value};
  504: field_spec({Var, Value}) -> #{var => Var, values => [Value]}.
  505: 
  506: required_modules() ->
  507:     [{mod_caps, config_parser_helper:mod_config_with_auto_backend(mod_caps)},
  508:      {mod_pubsub, mod_config(mod_pubsub, #{plugins => [<<"dag">>, <<"pep">>],
  509:                                            nodetree => nodetree_dag,
  510:                                            backend => mongoose_helper:mnesia_or_rdbms_backend(),
  511:                                            pep_mapping => #{},
  512:                                            host => subhost_pattern("pubsub.@HOST@")})}].
  513: required_modules(cache_tests) ->
  514:     HostType = domain_helper:host_type(),
  515:     [{mod_caps, config_parser_helper:mod_config_with_auto_backend(mod_caps)},
  516:      {mod_pubsub, mod_config(mod_pubsub, #{plugins => [<<"dag">>, <<"pep">>],
  517:                                            nodetree => nodetree_dag,
  518:                                            backend => mongoose_helper:mnesia_or_rdbms_backend(),
  519:                                            pep_mapping => #{},
  520:                                            host => subhost_pattern("pubsub.@HOST@"),
  521:                                            last_item_cache => mongoose_helper:mnesia_or_rdbms_backend()
  522:      })}].
  523: 
  524: send_initial_presence_with_caps(NodeNS, Client) ->
  525:     case is_caps_client(Client) of
  526:         false -> escalus_story:send_initial_presence(Client);
  527:         true -> send_presence_with_caps(Client, caps(NodeNS))
  528:     end.
  529: 
  530: handle_requested_caps(NodeNS, User) ->
  531:     case is_caps_client(User) of
  532:         false -> ok;
  533:         true -> DiscoRequest = escalus:wait_for_stanza(User),
  534:                 send_caps_disco_result(User, DiscoRequest, NodeNS)
  535:     end.
  536: 
  537: is_caps_client(Client) ->
  538:     case escalus_client:username(Client) of
  539:         <<"alice", _/binary>> -> false;
  540:         _ -> true
  541:     end.
  542: 
  543: send_presence_with_caps(User, Caps) ->
  544:     Presence = escalus_stanza:presence(<<"available">>, [Caps]),
  545:     escalus:send(User, Presence).
  546: 
  547: send_caps_disco_result(User, DiscoRequest, NodeNS) ->
  548:     QueryEl = escalus_stanza:query_el(?NS_DISCO_INFO, feature_elems(NodeNS)),
  549:     DiscoResult = escalus_stanza:iq_result(DiscoRequest, [QueryEl]),
  550:     escalus:send(User, DiscoResult).
  551: 
  552: receive_presence_with_caps(User1, User2, Caps) ->
  553:     PresenceNotification = escalus:wait_for_stanza(User1),
  554:     escalus:assert(is_presence, PresenceNotification),
  555:     escalus:assert(is_stanza_from, [User2], PresenceNotification),
  556:     Caps = exml_query:subelement(PresenceNotification, <<"c">>).
  557: 
  558: make_pep_node_info(Client, NodeName) ->
  559:     {escalus_utils:jid_to_lower(escalus_utils:get_short_jid(Client)), NodeName}.
  560: 
  561: verify_publish_options(FullNodeConfig, Options) ->
  562:     NodeConfig = [{Opt, Value} || {Opt, _, Value} <- FullNodeConfig],
  563:     Options = lists:filter(fun(Option) ->
  564:                                lists:member(Option, NodeConfig)
  565:                            end, Options).
  566: 
  567: set_caps(Config) ->
  568:     [{escalus_overrides, [{start_ready_clients, {?MODULE, start_caps_clients}}]},
  569:      {node_ns, random_node_ns()} | Config].
  570: 
  571: %% Implemented only for one resource per client, because it is enough
  572: start_caps_clients(Config, [{UserSpec, Resource}]) ->
  573:     NodeNS = ?config(node_ns, Config),
  574:     {ok, Client} = escalus_client:start(Config, UserSpec, Resource),
  575:     send_initial_presence_with_caps(NodeNS, Client),
  576:     escalus:assert(is_presence, escalus:wait_for_stanza(Client)),
  577:     handle_requested_caps(NodeNS, Client),
  578:     [Client].
  579: 
  580: %%-----------------------------------------------------------------
  581: %% XML helpers
  582: %%-----------------------------------------------------------------
  583: 
  584: feature_elems(PEPNodeNS) ->
  585:     [#xmlel{name = <<"identity">>,
  586:             attrs = [{<<"category">>, <<"client">>},
  587:                      {<<"name">>, <<"Psi">>},
  588:                      {<<"type">>, <<"pc">>}]} |
  589:      [feature_elem(F) || F <- features(PEPNodeNS)]].
  590: 
  591: feature_elem(F) ->
  592:     #xmlel{name = <<"feature">>,
  593:            attrs = [{<<"var">>, F}]}.
  594: 
  595: caps(PEPNodeNS) ->
  596:     #xmlel{name = <<"c">>,
  597:            attrs = [{<<"xmlns">>, ?NS_CAPS},
  598:                     {<<"hash">>, <<"sha-1">>},
  599:                     {<<"node">>, caps_node_name()},
  600:                     {<<"ver">>, caps_hash(PEPNodeNS)}]}.
  601: 
  602: features(PEPNodeNS) ->
  603:     [?NS_DISCO_INFO,
  604:      ?NS_DISCO_ITEMS,
  605:      ?NS_GEOLOC,
  606:      ns_notify(?NS_GEOLOC),
  607:      PEPNodeNS,
  608:      ns_notify(PEPNodeNS)].
  609: 
  610: ns_notify(NS) ->
  611:     <<NS/binary, "+notify">>.
  612: 
  613: random_node_ns() ->
  614:     base64:encode(crypto:strong_rand_bytes(16)).
  615: 
  616: caps_hash(PEPNodeNS) ->
  617:     rpc(mim(), mod_caps, make_disco_hash, [feature_elems(PEPNodeNS), sha1]).
  618: 
  619: caps_node_name() ->
  620:     <<"http://www.chatopus.com">>.
  621: 
  622: send_presence(From, Type, To) ->
  623:     ToJid = escalus_client:short_jid(To),
  624:     Stanza = escalus_stanza:presence_direct(ToJid, Type),
  625:     escalus_client:send(From, Stanza).
  626: 
  627: make_friends(Bob, Alice) ->
  628:     % makes uni-directional presence subscriptions while SM is disabled
  629:     % returns stanzas received finally by the inviter
  630:     send_presence(Bob, <<"subscribe">>, Alice),
  631:     escalus:assert(is_iq, escalus_client:wait_for_stanza(Bob)),
  632:     escalus:assert(is_presence, escalus_client:wait_for_stanza(Alice)),
  633:     send_presence(Alice, <<"subscribed">>, Bob),
  634:     escalus:assert(is_iq, escalus_client:wait_for_stanza(Alice)),
  635:     escalus:assert_many([is_message, is_iq]
  636:                         ++ lists:duplicate(2, is_presence),
  637:                         BobStanzas = escalus_client:wait_for_stanzas(Bob, 4)),
  638:     lists:filter(fun escalus_pred:is_message/1, BobStanzas).
  639: 
  640: make_friends_sm(Bob, Alice) ->
  641:     % makes uni-directional presence subscriptions while SM is enabled
  642:     % returns stanzas received finally by the inviter
  643:     send_presence(Bob, <<"subscribe">>, Alice),
  644:     escalus:assert_many([is_iq, is_sm_ack_request],
  645:                         escalus_client:wait_for_stanzas(Bob, 2)),
  646:     escalus:assert_many([is_presence, is_sm_ack_request],
  647:                         escalus_client:wait_for_stanzas(Alice, 2)),
  648:     send_presence(Alice, <<"subscribed">>, Bob),
  649:     escalus:assert_many([is_iq, is_sm_ack_request],
  650:                         escalus_client:wait_for_stanzas(Alice, 2)),
  651:     escalus:assert_many([is_message, is_iq]
  652:                         ++ lists:duplicate(2, is_presence)
  653:                         ++ lists:duplicate(4, is_sm_ack_request),
  654:                         BobStanzas = escalus_client:wait_for_stanzas(Bob, 8)),
  655:     lists:filter(fun escalus_pred:is_message/1, BobStanzas).
  656: 
  657: publish_with_sm(User, ItemId, Node, Options) ->
  658:     Id = id(User, Node, <<"publish">>),
  659:     Request = case proplists:get_value(with_payload, Options, true) of
  660:                   true -> escalus_pubsub_stanza:publish(User, ItemId, item_content(), Id, Node);
  661:                   false -> escalus_pubsub_stanza:publish(User, Id, Node)
  662:               end,
  663:     escalus_client:send(User, Request),
  664:     escalus:wait_for_stanzas(User, 2).
  665: 
  666: id(User, {NodeAddr, NodeName}, Suffix) ->
  667:     UserName = escalus_utils:get_username(User),
  668:     list_to_binary(io_lib:format("~s-~s-~s-~s", [UserName, NodeAddr, NodeName, Suffix])).
  669: 
  670: item_content() ->
  671:     #xmlel{name = <<"entry">>,
  672:         attrs = [{<<"xmlns">>, <<"http://www.w3.org/2005/Atom">>}]}.
  673: 
  674: enable_sm(User) ->
  675:     escalus_client:send(User, escalus_stanza:enable_sm()),
  676:     #xmlel{name = <<"enabled">>} = escalus:wait_for_stanza(User).