1: -module(push_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("common_test/include/ct.hrl").
    5: -include_lib("eunit/include/eunit.hrl").
    6: -include_lib("exml/include/exml.hrl").
    7: 
    8: -import(muc_light_helper,
    9:     [
   10:         room_bin_jid/1,
   11:         create_room/6
   12:     ]).
   13: -import(escalus_ejabberd, [rpc/3]).
   14: -import(distributed_helper, [subhost_pattern/1]).
   15: -import(domain_helper, [host_type/0]).
   16: -import(config_parser_helper, [mod_config/2, config/2]).
   17: -import(push_helper, [
   18:     enable_stanza/2, enable_stanza/3, enable_stanza/4,
   19:     disable_stanza/1, disable_stanza/2, become_unavailable/1
   20: ]).
   21: 
   22: -define(RPC_SPEC, distributed_helper:mim()).
   23: -define(SESSION_KEY, publish_service).
   24: 
   25: -define(VIRTUAL_PUBSUB_DOMAIN, <<"virtual.domain">>).
   26: 
   27: %%--------------------------------------------------------------------
   28: %% Suite configuration
   29: %%--------------------------------------------------------------------
   30: 
   31: all() ->
   32:     [
   33:         {group, disco},
   34:         {group, toggling},
   35:         {group, pubsub_ful},
   36:         {group, pubsub_less}
   37:     ].
   38: 
   39: groups() ->
   40:     G = [
   41:          {disco, [], [
   42:                       push_notifications_listed_disco_when_available,
   43:                       push_notifications_not_listed_disco_when_not_available
   44:                      ]},
   45:          {toggling, [parallel], [
   46:                                  enable_should_fail_with_missing_attributes,
   47:                                  enable_should_fail_with_invalid_attributes,
   48:                                  enable_should_succeed_without_form,
   49:                                  enable_with_form_should_fail_with_incorrect_from,
   50:                                  enable_should_accept_correct_from,
   51:                                  disable_should_fail_with_missing_attributes,
   52:                                  disable_should_fail_with_invalid_attributes,
   53:                                  disable_all,
   54:                                  disable_node,
   55:                                  disable_node_enabled_in_session_removes_it_from_session_info,
   56:                                  disable_all_nodes_removes_it_from_all_user_session_infos,
   57:                                  disable_node_enabled_in_other_session_leaves_current_info_unchanged
   58:                                 ]},
   59:          {pubsub_ful, [], notification_groups()},
   60:          {pubsub_less, [], notification_groups()},
   61:          {pm_msg_notifications, [parallel], [
   62:                                              pm_no_msg_notifications_if_not_enabled,
   63:                                              pm_no_msg_notifications_if_user_online,
   64:                                              pm_msg_notify_if_user_offline,
   65:                                              pm_msg_notify_if_user_offline_with_publish_options,
   66:                                              pm_msg_notify_stops_after_disabling,
   67:                                              pm_msg_notify_stops_after_removal
   68:                                             ]},
   69:          {muclight_msg_notifications, [parallel], [
   70:                                                    muclight_no_msg_notifications_if_not_enabled,
   71:                                                    muclight_no_msg_notifications_if_user_online,
   72:                                                    muclight_msg_notify_if_user_offline,
   73:                                                    muclight_msg_notify_if_user_offline_with_publish_options,
   74:                                                    muclight_msg_notify_stops_after_disabling
   75:                                                   ]}
   76:         ],
   77:     ct_helper:repeat_all_until_all_ok(G).
   78: 
   79: notification_groups() ->
   80:     [
   81:      {group, pm_msg_notifications},
   82:      {group, muclight_msg_notifications}
   83:     ].
   84: 
   85: suite() ->
   86:     escalus:suite().
   87: 
   88: %%--------------------------------------------------------------------
   89: %% Init & teardown
   90: %%--------------------------------------------------------------------
   91: 
   92: %% --------------------- Callbacks ------------------------
   93: 
   94: init_per_suite(Config) ->
   95:     %% For mocking with unnamed functions
   96:     mongoose_helper:inject_module(?MODULE),
   97:     escalus:init_per_suite(Config).
   98: end_per_suite(Config) ->
   99:     escalus_fresh:clean(),
  100:     escalus:end_per_suite(Config).
  101: 
  102: init_per_group(disco, Config) ->
  103:     escalus:create_users(Config, escalus:get_users([alice]));
  104: init_per_group(pubsub_ful, Config) ->
  105:     [{pubsub_host, real} | Config];
  106: init_per_group(pubsub_less, Config) ->
  107:     [{pubsub_host, virtual} | Config];
  108: init_per_group(muclight_msg_notifications = GroupName, Config0) ->
  109:     HostType = host_type(),
  110:     Config = dynamic_modules:save_modules(HostType, Config0),
  111:     dynamic_modules:ensure_modules(HostType, required_modules(GroupName)),
  112:     muc_light_helper:clear_db(HostType),
  113:     Config;
  114: init_per_group(GroupName, Config0) ->
  115:     HostType = host_type(),
  116:     Config = dynamic_modules:save_modules(HostType, Config0),
  117:     dynamic_modules:ensure_modules(HostType, required_modules(GroupName)),
  118:     Config.
  119: 
  120: end_per_group(disco, Config) ->
  121:     escalus:delete_users(Config),
  122:     Config;
  123: end_per_group(ComplexGroup, Config) when ComplexGroup == pubsub_ful;
  124:                                          ComplexGroup == pubsub_less ->
  125:     Config;
  126: end_per_group(_, Config) ->
  127:     dynamic_modules:restore_modules(Config),
  128:     Config.
  129: 
  130: init_per_testcase(CaseName = push_notifications_listed_disco_when_available, Config1) ->
  131:     HostType = host_type(),
  132:     Config2 = dynamic_modules:save_modules(HostType, Config1),
  133:     dynamic_modules:ensure_modules(HostType, required_modules(CaseName)),
  134:     escalus:init_per_testcase(CaseName, Config2);
  135: init_per_testcase(CaseName = push_notifications_not_listed_disco_when_not_available, Config) ->
  136:     escalus:init_per_testcase(CaseName, Config);
  137: init_per_testcase(CaseName, Config0) ->
  138:     Config1 = escalus_fresh:create_users(Config0, [{bob, 1}, {alice, 1}, {kate, 1}]),
  139:     Config2 = add_pubsub_jid([{case_name, CaseName} | Config1]),
  140: 
  141:     Config = case ?config(pubsub_host, Config0) of
  142:                  virtual ->
  143:                      start_hook_listener(Config2);
  144:                  _ ->
  145:                      start_route_listener(Config2)
  146:              end,
  147: 
  148:     escalus:init_per_testcase(CaseName, Config).
  149: 
  150: end_per_testcase(CaseName = push_notifications_listed_disco_when_available, Config) ->
  151:     dynamic_modules:restore_modules(Config),
  152:     escalus:end_per_testcase(CaseName, Config);
  153: end_per_testcase(CaseName = push_notifications_not_listed_disco_when_not_available, Config) ->
  154:     escalus:end_per_testcase(CaseName, Config);
  155: end_per_testcase(CaseName, Config) ->
  156:     case ?config(pubsub_host, Config) of
  157:         virtual ->
  158:             stop_hook_listener(Config);
  159:         _ ->
  160:             stop_route_listener(Config)
  161:     end,
  162:     escalus:end_per_testcase(CaseName, Config).
  163: 
  164: %% --------------------- Helpers ------------------------
  165: 
  166: required_modules(muclight_msg_notifications) ->
  167:     [pusher_module(), muc_light_module()];
  168: required_modules(_) ->
  169:     [pusher_module()].
  170: 
  171: pusher_module() ->
  172:     PushOpts = #{backend => mongoose_helper:mnesia_or_rdbms_backend(),
  173:                  virtual_pubsub_hosts => [subhost_pattern(?VIRTUAL_PUBSUB_DOMAIN)]},
  174:     {mod_event_pusher, #{push => config([modules, mod_event_pusher, push], PushOpts)}}.
  175: 
  176: muc_light_module() ->
  177:     {mod_muc_light,
  178:      mod_config(mod_muc_light, #{backend => mongoose_helper:mnesia_or_rdbms_backend(),
  179:                                  rooms_in_rosters => true})}.
  180: 
  181: %%--------------------------------------------------------------------
  182: %% GROUP disco
  183: %%--------------------------------------------------------------------
  184: 
  185: push_notifications_listed_disco_when_available(Config) ->
  186:     escalus:story(
  187:         Config, [{alice, 1}],
  188:         fun(Alice) ->
  189:             Server = escalus_client:server(Alice),
  190:             escalus:send(Alice, escalus_stanza:disco_info(Server)),
  191:             Stanza = escalus:wait_for_stanza(Alice),
  192:             escalus:assert(is_iq_result, Stanza),
  193:             escalus:assert(has_feature, [push_helper:ns_push()], Stanza),
  194:             ok
  195:         end).
  196: 
  197: push_notifications_not_listed_disco_when_not_available(Config) ->
  198:     escalus:story(
  199:         Config, [{alice, 1}],
  200:         fun(Alice) ->
  201:             Server = escalus_client:server(Alice),
  202:             escalus:send(Alice, escalus_stanza:disco_info(Server)),
  203:             Stanza = escalus:wait_for_stanza(Alice),
  204:             escalus:assert(is_iq_result, Stanza),
  205:             Pred = fun(Feature, Stanza0) -> not escalus_pred:has_feature(Feature, Stanza0) end,
  206:             escalus:assert(Pred, [push_helper:ns_push()], Stanza),
  207:             ok
  208:         end).
  209: 
  210: %%--------------------------------------------------------------------
  211: %% GROUP toggling
  212: %%--------------------------------------------------------------------
  213: 
  214: enable_should_fail_with_missing_attributes(Config) ->
  215:     escalus:story(
  216:         Config, [{bob, 1}],
  217:         fun(Bob) ->
  218:             BobJID = escalus_utils:get_jid(Bob),
  219: 
  220:             escalus:send(Bob, escalus_stanza:iq(<<"set">>, [#xmlel{name = <<"enable">>}])),
  221:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  222:                            escalus:wait_for_stanza(Bob)),
  223: 
  224:             CorrectAttrs = [{<<"xmlns">>, <<"urn:xmpp:push:0">>},
  225:                             {<<"jid">>, BobJID},
  226:                             {<<"node">>, <<"NodeKey">>}],
  227: 
  228:             %% Sending only one attribute should fail
  229:             lists:foreach(
  230:                 fun(Attr) ->
  231:                     escalus:send(Bob, escalus_stanza:iq(<<"set">>,
  232:                                                         [#xmlel{name = <<"enable">>,
  233:                                                                 attrs = [Attr]}])),
  234:                     escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  235:                                    escalus:wait_for_stanza(Bob))
  236:                 end, CorrectAttrs),
  237: 
  238:             %% Sending all but one attribute should fail
  239:             lists:foreach(
  240:                 fun(Attr) ->
  241:                     escalus:send(Bob, escalus_stanza:iq(<<"set">>,
  242:                                                         [#xmlel{name = <<"enable">>,
  243:                                                                 attrs = CorrectAttrs -- [Attr]}])),
  244:                     escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  245:                                    escalus:wait_for_stanza(Bob))
  246:                 end, CorrectAttrs),
  247: 
  248:             ok
  249:         end).
  250: 
  251: enable_should_fail_with_invalid_attributes(Config) ->
  252:     escalus:story(
  253:         Config, [{bob, 1}],
  254:         fun(Bob) ->
  255:             PubsubJID = pubsub_jid(Config),
  256: 
  257:             %% Empty JID
  258:             escalus:send(Bob, enable_stanza(<<>>, <<"nodeId">>)),
  259:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  260:                            escalus:wait_for_stanza(Bob)),
  261: 
  262:             %% Empty node
  263:             escalus:send(Bob, enable_stanza(PubsubJID, <<>>)),
  264:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  265:                            escalus:wait_for_stanza(Bob)),
  266:             ok
  267:         end).
  268: 
  269: 
  270: enable_should_succeed_without_form(Config) ->
  271:     escalus:story(
  272:         Config, [{bob, 1}],
  273:         fun(Bob) ->
  274:             PubsubJID = pubsub_jid(Config),
  275: 
  276:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>)),
  277:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  278: 
  279:             ok
  280:         end).
  281: 
  282: enable_with_form_should_fail_with_incorrect_from(Config) ->
  283:     escalus:story(
  284:         Config, [{bob, 1}],
  285:         fun(Bob) ->
  286:             PubsubJID = pubsub_jid(Config),
  287: 
  288:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>, [], <<"wrong">>)),
  289:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  290:                            escalus:wait_for_stanza(Bob)),
  291:             ok
  292:         end).
  293: 
  294: enable_should_accept_correct_from(Config) ->
  295:     escalus:story(
  296:         Config, [{bob, 1}],
  297:         fun(Bob) ->
  298:             PubsubJID = pubsub_jid(Config),
  299: 
  300:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>, [])),
  301:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  302: 
  303:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>, [
  304:                 {<<"secret1">>, <<"token1">>},
  305:                 {<<"secret2">>, <<"token2">>}
  306:             ])),
  307:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  308: 
  309:             ok
  310:         end).
  311: 
  312: disable_should_fail_with_missing_attributes(Config) ->
  313:     escalus:story(
  314:         Config, [{bob, 1}],
  315:         fun(Bob) ->
  316:             BobJID = escalus_utils:get_jid(Bob),
  317: 
  318:             escalus:send(Bob, escalus_stanza:iq(<<"set">>, [#xmlel{name = <<"disable">>}])),
  319:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  320:                            escalus:wait_for_stanza(Bob)),
  321: 
  322:             CorrectAttrs = [{<<"xmlns">>, <<"urn:xmpp:push:0">>}, {<<"jid">>, BobJID}],
  323: 
  324:             %% Sending only one attribute should fail
  325:             lists:foreach(
  326:                 fun(Attr) ->
  327:                     escalus:send(Bob, escalus_stanza:iq(<<"set">>,
  328:                                                         [#xmlel{name = <<"disable">>,
  329:                                                                 attrs = [Attr]}])),
  330:                     escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  331:                                    escalus:wait_for_stanza(Bob))
  332:                 end, CorrectAttrs),
  333:             ok
  334:         end).
  335: 
  336: disable_should_fail_with_invalid_attributes(Config) ->
  337:     escalus:story(
  338:         Config, [{bob, 1}],
  339:         fun(Bob) ->
  340:             %% Empty JID
  341:             escalus:send(Bob, disable_stanza(<<>>, <<"nodeId">>)),
  342:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  343:                            escalus:wait_for_stanza(Bob)),
  344:             escalus:send(Bob, disable_stanza(<<>>)),
  345:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  346:                            escalus:wait_for_stanza(Bob)),
  347:             ok
  348:         end).
  349: 
  350: disable_all(Config) ->
  351:     escalus:story(
  352:         Config, [{bob, 1}],
  353:         fun(Bob) ->
  354:             PubsubJID = pubsub_jid(Config),
  355: 
  356:             escalus:send(Bob, disable_stanza(PubsubJID)),
  357:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  358: 
  359:             ok
  360:         end).
  361: 
  362: disable_node(Config) ->
  363:     escalus:story(
  364:         Config, [{bob, 1}],
  365:         fun(Bob) ->
  366:             PubsubJID = pubsub_jid(Config),
  367: 
  368:             escalus:send(Bob, disable_stanza(PubsubJID, <<"NodeId">>)),
  369:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  370: 
  371:             ok
  372:         end).
  373: 
  374: disable_node_enabled_in_session_removes_it_from_session_info(Config) ->
  375:     escalus:fresh_story(
  376:         Config, [{bob, 1}],
  377:         fun(Bob) ->
  378:             PubsubJID = pubsub_jid(Config),
  379:             NodeId = pubsub_tools:pubsub_node_name(),
  380: 
  381:             escalus:send(Bob, enable_stanza(PubsubJID, NodeId, [])),
  382:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  383: 
  384:             Info = mongoose_helper:get_session_info(?RPC_SPEC, Bob),
  385:             {_JID, NodeId, _} = maps:get(?SESSION_KEY, Info),
  386: 
  387:             escalus:send(Bob, disable_stanza(PubsubJID, NodeId)),
  388:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  389: 
  390:             Info2 = mongoose_helper:get_session_info(?RPC_SPEC, Bob),
  391:             false = maps:get(?SESSION_KEY, Info2, false)
  392:         end).
  393: 
  394: disable_all_nodes_removes_it_from_all_user_session_infos(Config) ->
  395:     escalus:fresh_story(
  396:         Config, [{bob, 2}],
  397:         fun(Bob1, Bob2) ->
  398:             PubsubJID = pubsub_jid(Config),
  399:             NodeId = pubsub_tools:pubsub_node_name(),
  400:             NodeId2 = pubsub_tools:pubsub_node_name(),
  401: 
  402:             escalus:send(Bob1, enable_stanza(PubsubJID, NodeId, [])),
  403:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob1)),
  404: 
  405:             escalus:send(Bob2, enable_stanza(PubsubJID, NodeId2, [])),
  406:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob2)),
  407: 
  408:             Info = mongoose_helper:get_session_info(?RPC_SPEC, Bob1),
  409:             {JID, NodeId, _} = maps:get(?SESSION_KEY, Info),
  410: 
  411:             Info2 = mongoose_helper:get_session_info(?RPC_SPEC, Bob2),
  412:             {JID, NodeId2, _} = maps:get(?SESSION_KEY, Info2),
  413: 
  414:             %% Now Bob1 disables all nodes
  415:             escalus:send(Bob1, disable_stanza(PubsubJID)),
  416:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob1)),
  417: 
  418:             %% And we check if Bob1 and Bob2 have push notifications cleared from session info
  419:             Info3 = mongoose_helper:get_session_info(?RPC_SPEC, Bob1),
  420:             false = maps:get(?SESSION_KEY, Info3, false),
  421: 
  422:             Info4 = mongoose_helper:get_session_info(?RPC_SPEC, Bob2),
  423:             false = maps:get(?SESSION_KEY, Info4, false)
  424:         end).
  425: 
  426: disable_node_enabled_in_other_session_leaves_current_info_unchanged(Config) ->
  427:     escalus:fresh_story(
  428:         Config, [{bob, 2}],
  429:         fun(Bob1, Bob2) ->
  430:             PubsubJID = pubsub_jid(Config),
  431:             NodeId = pubsub_tools:pubsub_node_name(),
  432:             NodeId2 = pubsub_tools:pubsub_node_name(),
  433: 
  434:             escalus:send(Bob1, enable_stanza(PubsubJID, NodeId, [])),
  435:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob1)),
  436: 
  437:             escalus:send(Bob2, enable_stanza(PubsubJID, NodeId2, [])),
  438:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob2)),
  439: 
  440:             Info = mongoose_helper:get_session_info(?RPC_SPEC, Bob1),
  441:             {JID, NodeId, _} = maps:get(?SESSION_KEY, Info),
  442: 
  443:             Info2 = mongoose_helper:get_session_info(?RPC_SPEC, Bob2),
  444:             {JID, NodeId2, _} = maps:get(?SESSION_KEY, Info2),
  445: 
  446:             %% Now Bob1 disables the node registered by Bob2
  447:             escalus:send(Bob1, disable_stanza(PubsubJID, NodeId)),
  448:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob1)),
  449: 
  450:             %% And we check if Bob1 still has its own Node in the session info
  451:             Info3 = mongoose_helper:get_session_info(?RPC_SPEC, Bob1),
  452:             false = maps:get(?SESSION_KEY, Info3, false)
  453:         end).
  454: 
  455: %%--------------------------------------------------------------------
  456: %% GROUP pm_msg_notifications
  457: %%--------------------------------------------------------------------
  458: 
  459: pm_no_msg_notifications_if_not_enabled(Config) ->
  460:     escalus:story(
  461:         Config, [{bob, 1}, {alice, 1}],
  462:         fun(Bob, Alice) ->
  463:             become_unavailable(Bob),
  464:             escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)),
  465: 
  466:             ?assert(not truly(received_push())),
  467:             ok
  468:         end).
  469: 
  470: pm_no_msg_notifications_if_user_online(Config) ->
  471:     escalus:story(
  472:         Config, [{bob, 1}, {alice, 1}],
  473:         fun(Bob, Alice) ->
  474:             PubsubJID = pubsub_jid(Config),
  475: 
  476:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>)),
  477:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  478: 
  479:             escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)),
  480: 
  481:             ?assert(not truly(received_push())),
  482:             ok
  483:         end).
  484: 
  485: pm_msg_notify_if_user_offline(Config) ->
  486:     escalus:story(
  487:         Config, [{bob, 1}, {alice, 1}],
  488:         fun(Bob, Alice) ->
  489:             PubsubJID = pubsub_jid(Config),
  490: 
  491:             AliceJID = bare_jid(Alice),
  492:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>)),
  493:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  494:             become_unavailable(Bob),
  495: 
  496:             escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)),
  497: 
  498:             #{ payload := Payload } = received_push(),
  499:             ?assertMatch(<<"OH, HAI!">>, proplists:get_value(<<"last-message-body">>, Payload)),
  500:             ?assertMatch(AliceJID,
  501:                          proplists:get_value(<<"last-message-sender">>, Payload)),
  502: 
  503:             ok
  504:         end).
  505: 
  506: pm_msg_notify_if_user_offline_with_publish_options(Config) ->
  507:     escalus:story(
  508:         Config, [{bob, 1}, {alice, 1}],
  509:         fun(Bob, Alice) ->
  510:             PubsubJID = pubsub_jid(Config),
  511: 
  512:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>,
  513:                                             [{<<"field1">>, <<"value1">>},
  514:                                              {<<"field2">>, <<"value2">>}])),
  515:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  516:             become_unavailable(Bob),
  517: 
  518:             escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)),
  519: 
  520:             #{ publish_options := PublishOptions } = received_push(),
  521: 
  522:             ?assertMatch(<<"value1">>, proplists:get_value(<<"field1">>, PublishOptions)),
  523:             ?assertMatch(<<"value2">>, proplists:get_value(<<"field2">>, PublishOptions)),
  524:             ok
  525:         end).
  526: 
  527: pm_msg_notify_stops_after_disabling(Config) ->
  528:     escalus:story(
  529:         Config, [{bob, 1}, {alice, 1}],
  530:         fun(Bob, Alice) ->
  531:             PubsubJID = pubsub_jid(Config),
  532: 
  533:             %% Enable
  534:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>, [])),
  535:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  536: 
  537:             %% Disable
  538:             escalus:send(Bob, disable_stanza(PubsubJID, <<"NodeId">>)),
  539:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  540:             become_unavailable(Bob),
  541: 
  542:             escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)),
  543: 
  544:             ?assert(not received_push()),
  545: 
  546:             ok
  547:         end).
  548: 
  549: pm_msg_notify_stops_after_removal(Config) ->
  550:     PubsubJID = pubsub_jid(Config),
  551:     escalus:story(
  552:         Config, [{bob, 1}],
  553:         fun(Bob) ->
  554:             %% Enable
  555:             escalus:send(Bob, enable_stanza(PubsubJID, <<"NodeId">>, [])),
  556:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  557: 
  558:             %% Remove account - this should disable the notifications
  559:             Pid = mongoose_helper:get_session_pid(Bob, distributed_helper:mim()),
  560:             escalus_connection:send(Bob, escalus_stanza:remove_account()),
  561:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Bob)),
  562:             mongoose_helper:wait_for_pid_to_die(Pid)
  563:         end),
  564:     BobUser = lists:keyfind(bob, 1, escalus_config:get_config(escalus_users, Config)),
  565:     escalus_users:create_user(Config, BobUser),
  566:     escalus:story(
  567:         Config, [{bob, 1}, {alice, 1}],
  568:         fun(Bob, Alice) ->
  569:             become_unavailable(Bob),
  570:             escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)),
  571:             ?assert(not truly(received_push()))
  572:         end).
  573: 
  574: %%--------------------------------------------------------------------
  575: %% GROUP muclight_msg_notifications
  576: %%--------------------------------------------------------------------
  577: 
  578: muclight_no_msg_notifications_if_not_enabled(Config) ->
  579:     escalus:story(
  580:         Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  581:         fun(Alice, Bob, Kate) ->
  582:             Room = room_name(Config),
  583:             create_room(Room, [bob, alice, kate], Config),
  584:             become_unavailable(Alice),
  585:             become_unavailable(Kate),
  586: 
  587:             Msg = <<"Heyah!">>,
  588:             Stanza = escalus_stanza:groupchat_to(room_bin_jid(Room), Msg),
  589: 
  590:             escalus:send(Bob, Stanza),
  591: 
  592:             ?assert(not truly(received_push())),
  593: 
  594:             ok
  595:         end).
  596: 
  597: muclight_no_msg_notifications_if_user_online(Config) ->
  598:     escalus:story(
  599:         Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  600:         fun(Alice, Bob, Kate) ->
  601:             Room = room_name(Config),
  602:             PubsubJID = pubsub_jid(Config),
  603: 
  604:             create_room(Room, [bob, alice, kate], Config),
  605:             escalus:send(Alice, enable_stanza(PubsubJID, <<"NodeId">>)),
  606:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  607:             become_unavailable(Kate),
  608: 
  609:             Msg = <<"Heyah!">>,
  610:             Stanza = escalus_stanza:groupchat_to(room_bin_jid(Room), Msg),
  611:             escalus:send(Bob, Stanza),
  612: 
  613:             ?assert(not truly(received_push())),
  614:             ok
  615:         end).
  616: 
  617: muclight_msg_notify_if_user_offline(Config) ->
  618:     escalus:story(
  619:         Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  620:         fun(Alice, Bob, _Kate) ->
  621:             PubsubJID = pubsub_jid(Config),
  622:             Room = room_name(Config),
  623:             BobJID = bare_jid(Bob),
  624: 
  625:             create_room(Room, [bob, alice, kate], Config),
  626:             escalus:send(Alice, enable_stanza(PubsubJID, <<"NodeId">>)),
  627:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  628:             become_unavailable(Alice),
  629: 
  630:             Msg = <<"Heyah!">>,
  631:             Stanza = escalus_stanza:groupchat_to(room_bin_jid(Room), Msg),
  632:             escalus:send(Bob, Stanza),
  633: 
  634:             #{ payload := Payload } = received_push(),
  635: 
  636:             ?assertMatch(Msg, proplists:get_value(<<"last-message-body">>, Payload)),
  637:             SenderId = <<(room_bin_jid(Room))/binary, "/" ,BobJID/binary>>,
  638:             ?assertMatch(SenderId,
  639:                          proplists:get_value(<<"last-message-sender">>, Payload)),
  640:             ok
  641:         end).
  642: 
  643: muclight_msg_notify_if_user_offline_with_publish_options(Config) ->
  644:     escalus:story(
  645:         Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  646:         fun(Alice, Bob, _Kate) ->
  647:             PubsubJID = pubsub_jid(Config),
  648:             Room = room_name(Config),
  649: 
  650:             create_room(Room, [bob, alice, kate], Config),
  651:             escalus:send(Alice, enable_stanza(PubsubJID, <<"NodeId">>,
  652:                                             [{<<"field1">>, <<"value1">>},
  653:                                              {<<"field2">>, <<"value2">>}])),
  654:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  655:             become_unavailable(Alice),
  656: 
  657:             Msg = <<"Heyah!">>,
  658:             Stanza = escalus_stanza:groupchat_to(room_bin_jid(Room), Msg),
  659:             escalus:send(Bob, Stanza),
  660: 
  661:             #{ publish_options := PublishOptions } = received_push(),
  662: 
  663:             ?assertMatch(<<"value1">>, proplists:get_value(<<"field1">>, PublishOptions)),
  664:             ?assertMatch(<<"value2">>, proplists:get_value(<<"field2">>, PublishOptions)),
  665:             ok
  666:         end).
  667: 
  668: muclight_msg_notify_stops_after_disabling(Config) ->
  669:     escalus:story(
  670:         Config, [{alice, 1}, {bob, 1}, {kate, 1}],
  671:         fun(Alice, Bob, _Kate) ->
  672:             Room = room_name(Config),
  673:             PubsubJID = pubsub_jid(Config),
  674:             create_room(Room, [bob, alice, kate], Config),
  675: 
  676:             %% Enable
  677:             escalus:send(Alice, enable_stanza(PubsubJID, <<"NodeId">>)),
  678:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  679: 
  680:             %% Disable
  681:             escalus:send(Alice, disable_stanza(PubsubJID, <<"NodeId">>)),
  682:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  683:             become_unavailable(Alice),
  684: 
  685:             Msg = <<"Heyah!">>,
  686:             Stanza = escalus_stanza:groupchat_to(room_bin_jid(Room), Msg),
  687:             escalus:send(Bob, Stanza),
  688: 
  689:             ?assert(not truly(received_push())),
  690:             ok
  691:         end).
  692: 
  693: %%--------------------------------------------------------------------
  694: %% Remote code
  695: %% Functions that will be executed in MongooseIM context + helpers that set them up
  696: %%--------------------------------------------------------------------
  697: 
  698: start_route_listener(Config) ->
  699:     %% We put namespaces in the state to avoid injecting push_helper module to MIM as well
  700:     State = #{ pid => self(),
  701:                pub_options_ns => push_helper:ns_pubsub_pub_options(),
  702:                push_form_ns => push_helper:push_form_type() },
  703:     Handler = rpc(mongoose_packet_handler, new, [?MODULE, #{state => State}]),
  704:     Domain = pubsub_domain(Config),
  705:     rpc(mongoose_router, register_route, [Domain, Handler]),
  706:     Config.
  707: 
  708: stop_route_listener(Config) ->
  709:     Domain = pubsub_domain(Config),
  710:     rpc(mongoose_router, unregister_route, [Domain]).
  711: 
  712: process_packet(Acc, _From, To, El, #{state := State}) ->
  713:     #{ pid := TestCasePid, pub_options_ns := PubOptionsNS, push_form_ns := PushFormNS } = State,
  714:     PublishXML = exml_query:path(El, [{element, <<"pubsub">>},
  715:                                       {element, <<"publish-options">>},
  716:                                       {element, <<"x">>}]),
  717:     PublishOptions = parse_form(PublishXML),
  718: 
  719:     PayloadXML = exml_query:path(El, [{element, <<"pubsub">>},
  720:                                       {element, <<"publish">>},
  721:                                       {element, <<"item">>},
  722:                                       {element, <<"notification">>},
  723:                                       {element, <<"x">>}]),
  724:     Payload = parse_form(PayloadXML),
  725: 
  726:     case valid_ns_if_defined(PubOptionsNS, PublishOptions) andalso
  727:          valid_ns_if_defined(PushFormNS, Payload) of
  728:         true ->
  729:             TestCasePid ! push_notification(jid:to_binary(To), Payload, PublishOptions);
  730:         false ->
  731:             %% We use publish_options0 and payload0 to avoid accidental match in received_push
  732:             %% even after some tests updates and refactors
  733:             TestCasePid ! #{ error => invalid_namespace,
  734:                              publish_options0 => PublishOptions,
  735:                              payload0 => Payload }
  736:     end,
  737:     Acc.
  738: 
  739: parse_form(undefined) ->
  740:     undefined;
  741: parse_form(#xmlel{name = <<"x">>} = Form) ->
  742:     parse_form(exml_query:subelements(Form, <<"field">>));
  743: parse_form(Fields) when is_list(Fields) ->
  744:     lists:map(
  745:         fun(Field) ->
  746:             {exml_query:attr(Field, <<"var">>),
  747:              exml_query:path(Field, [{element, <<"value">>}, cdata])}
  748:         end, Fields).
  749: 
  750: valid_ns_if_defined(_, undefined) ->
  751:     true;
  752: valid_ns_if_defined(NS, FormProplist) ->
  753:     NS =:= proplists:get_value(<<"FORM_TYPE">>, FormProplist).
  754: 
  755: start_hook_listener(Config) ->
  756:     TestCasePid = self(),
  757:     PubSubJID = pubsub_jid(Config),
  758:     rpc(?MODULE, rpc_start_hook_handler, [TestCasePid, PubSubJID]),
  759:     [{pid, TestCasePid}, {jid, PubSubJID} | Config].
  760: 
  761: stop_hook_listener(Config) ->
  762:     TestCasePid = proplists:get_value(pid, Config),
  763:     PubSubJID = proplists:get_value(jid, Config),
  764:     rpc(?MODULE, rpc_stop_hook_handler, [TestCasePid, PubSubJID]).
  765: 
  766: rpc_start_hook_handler(TestCasePid, PubSubJID) ->
  767:     gen_hook:add_handler(push_notifications,  <<"localhost">>,
  768:                          fun ?MODULE:hook_handler_fn/3,
  769:                          #{pid => TestCasePid, jid => PubSubJID}, 50).
  770: 
  771: hook_handler_fn(Acc,
  772:                 #{notification_forms := [PayloadMap], options := OptionMap} = _Params,
  773:                 #{pid := TestCasePid, jid := PubSubJID} = _Extra) ->
  774:     try jid:to_binary(mongoose_acc:get(push_notifications, pubsub_jid, Acc)) of
  775:         PubSubJIDBin when PubSubJIDBin =:= PubSubJID ->
  776:             TestCasePid ! push_notification(PubSubJIDBin,
  777:                                             maps:to_list(PayloadMap),
  778:                                             maps:to_list(OptionMap));
  779:         _ -> ok
  780:     catch
  781:         C:R:S ->
  782:             TestCasePid ! #{event => handler_error,
  783:                             class => C,
  784:                             reason => R,
  785:                             stacktrace => S}
  786:     end,
  787:     {ok, Acc}.
  788: 
  789: rpc_stop_hook_handler(TestCasePid, PubSubJID) ->
  790:     gen_hook:delete_handler(push_notifications, <<"localhost">>,
  791:                             fun ?MODULE:hook_handler_fn/3,
  792:                             #{pid => TestCasePid, jid => PubSubJID}, 50).
  793: 
  794: %%--------------------------------------------------------------------
  795: %% Test helpers
  796: %%--------------------------------------------------------------------
  797: 
  798: create_room(Room, [Owner | Members], Config) ->
  799:     Domain = domain_helper:domain(),
  800:     create_room(Room, <<"muclight.", Domain/binary>>, Owner, Members,
  801:                                 Config, <<"v1">>).
  802: 
  803: received_push() ->
  804:     receive
  805:         #{ push_notification := true } = Push -> Push
  806:     after
  807:         timer:seconds(5) ->
  808:             ct:pal("~p", [#{ result => nomatch, msg_inbox => process_info(self(), messages) }]),
  809:             false
  810:     end.
  811: 
  812: truly(false) ->
  813:     false;
  814: truly(#{ push_notification := true }) ->
  815:     true.
  816: 
  817: push_notification(PubsubJID, Payload, PublishOpts) ->
  818:     #{push_notification => true, pubsub_jid_bin => PubsubJID,
  819:       publish_options => PublishOpts, payload => Payload}.
  820: 
  821: bare_jid(JIDOrClient) ->
  822:     ShortJID = escalus_client:short_jid(JIDOrClient),
  823:     list_to_binary(string:to_lower(binary_to_list(ShortJID))).
  824: 
  825: add_pubsub_jid(Config) ->
  826:     CaseName = proplists:get_value(case_name, Config),
  827:     CaseNameBin = atom_to_binary(CaseName, utf8),
  828:     NameSuffix = uniq_name_suffix(),
  829:     UniqID = <<CaseNameBin/binary, "_", NameSuffix/binary>>,
  830:     {PubSubNodeName, PubSubDomain} =
  831:         case ?config(pubsub_host, Config) of
  832:             virtual ->
  833:                 %% unique node name, but preconfigured domain name
  834:                 {UniqID, ?VIRTUAL_PUBSUB_DOMAIN};
  835:             _ ->
  836:                 %% any node name, but unique domain. unique domain
  837:                 %% is required to intercept safely message routing
  838:                 {<<"pub-sub">>, UniqID}
  839:         end,
  840:     PubSubJID = <<PubSubNodeName/binary, "@", PubSubDomain/binary>>,
  841:     [{pubsub_jid, PubSubJID}, {pubsub_domain, PubSubDomain} | Config].
  842: 
  843: uniq_name_suffix() ->
  844:     {_, S, US} = erlang:timestamp(),
  845:     L = lists:flatten([integer_to_list(S rem 100), ".", integer_to_list(US)]),
  846:     list_to_binary(L).
  847: 
  848: pubsub_domain(Config) ->
  849:     proplists:get_value(pubsub_domain, Config).
  850: 
  851: pubsub_jid(Config) ->
  852:     proplists:get_value(pubsub_jid, Config).
  853: 
  854: room_name(Config) ->
  855:     CaseName = proplists:get_value(case_name, Config),
  856:     <<"room_", (atom_to_binary(CaseName, utf8))/binary>>.