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