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