1: -module(push_pubsub_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: -include_lib("eunit/include/eunit.hrl").
    4: -include_lib("exml/include/exml.hrl").
    5: -include("push_helper.hrl").
    6: 
    7: -import(distributed_helper, [subhost_pattern/1]).
    8: -import(domain_helper, [domain/0]).
    9: -import(config_parser_helper, [config/2]).
   10: 
   11: %%--------------------------------------------------------------------
   12: %% Suite configuration
   13: %%--------------------------------------------------------------------
   14: 
   15: all() ->
   16:     [
   17:         {group, disco},
   18:         {group, allocate},
   19:         {group, pubsub_publish},
   20:         {group, rest_integration_v2}
   21:     ].
   22: 
   23: groups() ->
   24:     G = [
   25:          {disco, [], [has_disco_identity]},
   26:          {allocate, [], [allocate_basic_node]},
   27:          {pubsub_publish, [], [
   28:                                publish_fails_with_invalid_item,
   29:                                publish_fails_with_no_options,
   30:                                publish_succeeds_with_valid_options,
   31:                                push_node_can_be_configured_to_whitelist_publishers
   32:                               ]},
   33:          {rest_integration_v2, [], [
   34:                                     rest_service_called_with_correct_path_v2,
   35:                                     rest_service_gets_correct_payload_v2,
   36:                                     rest_service_gets_correct_payload_silent_v2
   37:                                    ]}
   38:         ],
   39:     ct_helper:repeat_all_until_all_ok(G).
   40: 
   41: suite() ->
   42:     escalus:suite().
   43: 
   44: %%--------------------------------------------------------------------
   45: %% Init & teardown
   46: %%--------------------------------------------------------------------
   47: 
   48: init_per_suite(Config) ->
   49:     application:ensure_all_started(cowboy),
   50: 
   51:     %% For mocking with unnamed functions
   52:     mongoose_helper:inject_module(?MODULE),
   53: 
   54:     %% Start modules
   55:     Config2 = dynamic_modules:save_modules(domain(), Config),
   56:     Config3 = escalus:init_per_suite(Config2),
   57:     escalus:create_users(Config3, escalus:get_users([bob, alice])).
   58: end_per_suite(Config) ->
   59:     escalus_fresh:clean(),
   60:     dynamic_modules:restore_modules(Config),
   61:     escalus:delete_users(Config, escalus:get_users([bob, alice])),
   62:     escalus:end_per_suite(Config).
   63: 
   64: init_per_group(rest_integration_v1, Config) ->
   65:     restart_modules(Config, <<"v1">>);
   66: init_per_group(rest_integration_v2, Config) ->
   67:     restart_modules(Config, <<"v2">>);
   68: init_per_group(_, Config) ->
   69:     restart_modules(Config, <<"v2">>).
   70: 
   71: end_per_group(_, Config) ->
   72:     Config.
   73: 
   74: init_per_testcase(CaseName, Config) ->
   75:     MongoosePushMockPort = setup_mock_rest(),
   76: 
   77:     %% Start HTTP pool
   78:     PoolOpts = #{strategy => available_worker, workers => 20},
   79:     ConnOpts = #{host => "http://localhost:" ++ integer_to_list(MongoosePushMockPort),
   80:                  request_timeout => 2000},
   81:     Pool = config([outgoing_pools, http, mongoose_push_http],
   82:                   #{opts => PoolOpts, conn_opts => ConnOpts}),
   83:     [{ok, _Pid}] = rpc(mongoose_wpool, start_configured_pools, [[Pool]]),
   84:     escalus:init_per_testcase(CaseName, Config).
   85: 
   86: end_per_testcase(CaseName, Config) ->
   87:     rpc(mongoose_wpool, stop, [http, global, mongoose_push_http]),
   88:     teardown_mock_rest(),
   89:     escalus:end_per_testcase(CaseName, Config).
   90: 
   91: %%--------------------------------------------------------------------
   92: %% GROUP disco
   93: %%--------------------------------------------------------------------
   94: has_disco_identity(Config) ->
   95:     escalus:story(
   96:         Config, [{alice, 1}],
   97:         fun(Alice) ->
   98:             Server = pubsub_tools:node_addr(?PUBSUB_SUB_DOMAIN ++ "."),
   99:             escalus:send(Alice, escalus_stanza:disco_info(Server)),
  100:             Stanza = escalus:wait_for_stanza(Alice),
  101:             escalus:assert(has_identity, [<<"pubsub">>, <<"push">>], Stanza)
  102:         end).
  103: 
  104: %%--------------------------------------------------------------------
  105: %% GROUP allocate
  106: %%--------------------------------------------------------------------
  107: 
  108: allocate_basic_node(Config) ->
  109:     escalus:story(
  110:         Config, [{alice, 1}],
  111:         fun(Alice) ->
  112:             Node = push_pubsub_node(),
  113:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}])
  114:         end).
  115: 
  116: %%--------------------------------------------------------------------
  117: %% GROUP pubsub_publish
  118: %%--------------------------------------------------------------------
  119: 
  120: publish_fails_with_invalid_item(Config) ->
  121:     escalus:story(
  122:         Config, [{alice, 1}],
  123:         fun(Alice) ->
  124:             Node = push_pubsub_node(),
  125:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}]),
  126: 
  127:             Item =
  128:                 #xmlel{name = <<"invalid-item">>,
  129:                        attrs = [{<<"xmlns">>, ?NS_PUSH}]},
  130:             
  131:             Options = [
  132:                 {<<"device_id">>, <<"sometoken">>},
  133:                 {<<"service">>, <<"apns">>}
  134:             ],
  135: 
  136:             Publish = escalus_pubsub_stanza:publish_with_options(Alice, <<"itemid">>, Item,
  137:                                                                  <<"id">>, Node, Options),
  138:             escalus:send(Alice, Publish),
  139:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  140:                            escalus:wait_for_stanza(Alice)),
  141: 
  142:             ok
  143: 
  144:         end).
  145: 
  146: publish_fails_with_no_options(Config) ->
  147:     escalus:story(
  148:         Config, [{alice, 1}],
  149:         fun(Alice) ->
  150:             Node = push_pubsub_node(),
  151:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}]),
  152: 
  153:             ContentFields = [
  154:                 {<<"message-count">>, <<"1">>},
  155:                 {<<"last-message-sender">>, <<"senderId">>},
  156:                 {<<"last-message-body">>, <<"message body">>}
  157:             ],
  158: 
  159:             Item =
  160:                 #xmlel{name = <<"notification">>,
  161:                        attrs = [{<<"xmlns">>, ?NS_PUSH}],
  162:                        children = push_helper:maybe_form(ContentFields, ?PUSH_FORM_TYPE)},
  163: 
  164:             Publish = escalus_pubsub_stanza:publish(Alice, <<"itemid">>, Item, <<"id">>, Node),
  165:             escalus:send(Alice, Publish),
  166:             escalus:assert(is_error, [<<"cancel">>, <<"conflict">>],
  167:                            escalus:wait_for_stanza(Alice)),
  168: 
  169:             ok
  170: 
  171:         end).
  172: 
  173: publish_succeeds_with_valid_options(Config) ->
  174:     escalus:story(
  175:         Config, [{alice, 1}],
  176:         fun(Alice) ->
  177:             Node = push_pubsub_node(),
  178:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}]),
  179: 
  180:             Content = [
  181:                 {<<"message-count">>, <<"1">>},
  182:                 {<<"last-message-sender">>, <<"senderId">>},
  183:                 {<<"last-message-body">>, <<"message body">>}
  184:             ],
  185: 
  186:             Options = [
  187:                 {<<"device_id">>, <<"sometoken">>},
  188:                 {<<"service">>, <<"apns">>}
  189:             ],
  190: 
  191:             PublishIQ = publish_iq(Alice, Node, Content, Options),
  192:             escalus:send(Alice, PublishIQ),
  193:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  194: 
  195:             ok
  196: 
  197:         end).
  198: 
  199: push_node_can_be_configured_to_whitelist_publishers(Config) ->
  200:     escalus:story(
  201:         Config, [{alice, 1}, {bob, 1}],
  202:         fun(Alice, Bob) ->
  203:             Node = push_pubsub_node(),
  204:             Configuration = [{<<"pubsub#access_model">>, <<"whitelist">>},
  205:                              {<<"pubsub#publish_model">>, <<"publishers">>}],
  206:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>},
  207:                                                    {config, Configuration}]),
  208: 
  209:             ActiveConfig = pubsub_tools:get_configuration(Alice, Node, []),
  210:             ?assertMatch({_, _, <<"whitelist">>}, lists:keyfind(<<"pubsub#access_model">>, 1, ActiveConfig)),
  211:             ?assertMatch({_, _, <<"publishers">>}, lists:keyfind(<<"pubsub#publish_model">>, 1, ActiveConfig)),
  212: 
  213:             Content = [
  214:                 {<<"message-count">>, <<"1">>},
  215:                 {<<"last-message-sender">>, <<"senderId">>},
  216:                 {<<"last-message-body">>, <<"message body">>}
  217:             ],
  218: 
  219:             Options = [
  220:                 {<<"device_id">>, <<"sometoken">>},
  221:                 {<<"service">>, <<"apns">>}
  222:             ],
  223: 
  224:             PublishIQ = publish_iq(Bob, Node, Content, Options),
  225:             escalus:send(Bob, PublishIQ),
  226:             escalus:assert(is_error, [<<"auth">>, <<"forbidden">>],
  227:                            escalus:wait_for_stanza(Bob)),
  228: 
  229: 
  230:             ok
  231: 
  232:         end).
  233: 
  234: %%--------------------------------------------------------------------
  235: %% GROUP rest_integration
  236: %%--------------------------------------------------------------------
  237: 
  238: 
  239: rest_service_called_with_correct_path(Version, Config) ->
  240:     escalus:story(
  241:         Config, [{alice, 1}],
  242:         fun(Alice) ->
  243:             Node = setup_pubsub(Alice),
  244:             {Notification, Options} = prepare_notification(),
  245:             send_notification(Alice, Node, Notification, Options),
  246:             {Req, _Body} = get_mocked_request(),
  247: 
  248:             ?assertMatch(<<"POST">>, cowboy_req:method(Req)),
  249:             ?assertMatch(Version, cowboy_req:binding(level1, Req)),
  250:             ?assertMatch(<<"notification">>, cowboy_req:binding(level2, Req)),
  251:             ?assertMatch(<<"sometoken">>, cowboy_req:binding(level3, Req)),
  252:             ?assertMatch(undefined, cowboy_req:binding(level4, Req))
  253:         end).
  254: 
  255: rest_service_called_with_correct_path_v1(Config) ->
  256:     rest_service_called_with_correct_path(<<"v1">>, Config).
  257: 
  258: rest_service_called_with_correct_path_v2(Config) ->
  259:     rest_service_called_with_correct_path(<<"v2">>, Config).
  260: 
  261: 
  262: rest_service_gets_correct_payload_v1(Config) ->
  263:     escalus:story(
  264:         Config, [{alice, 1}],
  265:         fun(Alice) ->
  266:             Node = setup_pubsub(Alice),
  267:             {Notification, Options} = prepare_notification(),
  268:             send_notification(Alice, Node, Notification, Options),
  269:             {_, Body} = get_mocked_request(),
  270: 
  271:             ?assertMatch(#{<<"service">> := <<"some_awesome_service">>}, Body),
  272:             ?assertMatch(#{<<"badge">> := 876}, Body),
  273:             ?assertMatch(#{<<"title">> := <<"senderId">>}, Body),
  274:             ?assertMatch(#{<<"tag">> := <<"senderId">>}, Body),
  275:             ?assertMatch(#{<<"mode">> := <<"selected_mode">>}, Body),
  276:             ?assertMatch(#{<<"body">> := <<"message body 576364!!">>}, Body)
  277:         end).
  278: 
  279: rest_service_gets_correct_payload_v2(Config) ->
  280:     escalus:story(
  281:         Config, [{alice, 1}],
  282:         fun(Alice) ->
  283:             Node = setup_pubsub(Alice),
  284:             {Notification, Options} = prepare_notification(),
  285:             send_notification(Alice, Node, Notification, Options),
  286:             {_, Body} = get_mocked_request(),
  287: 
  288: 
  289:             ?assertMatch(#{<<"service">> := <<"some_awesome_service">>}, Body),
  290:             ?assertMatch(#{<<"mode">> := <<"selected_mode">>}, Body),
  291:             ?assertMatch(#{<<"topic">> := <<"some_topic">>}, Body),
  292:             ?assert(not maps:is_key(<<"data">>, Body)),
  293:             ?assertMatch(#{<<"alert">> := #{<<"badge">> := 876}}, Body),
  294:             ?assertMatch(#{<<"alert">> := #{<<"title">> := <<"senderId">>}}, Body),
  295:             ?assertMatch(#{<<"alert">> := #{<<"tag">> := <<"senderId">>}}, Body),
  296:             ?assertMatch(#{<<"alert">> := #{<<"body">> := <<"message body 576364!!">>}}, Body)
  297: 
  298:         end).
  299: 
  300: rest_service_gets_correct_payload_silent_v2(Config) ->
  301:     escalus:story(
  302:         Config, [{alice, 1}],
  303:         fun(Alice) ->
  304:             Node = setup_pubsub(Alice),
  305:             {Notification, Options} = prepare_notification([{<<"silent">>, <<"true">>}]),
  306:             send_notification(Alice, Node, Notification, Options),
  307:             {_, Body} = get_mocked_request(),
  308: 
  309:             ?assertMatch(#{<<"service">> := <<"some_awesome_service">>}, Body),
  310:             ?assertMatch(#{<<"mode">> := <<"selected_mode">>}, Body),
  311:             ?assertMatch(#{<<"topic">> := <<"some_topic">>}, Body),
  312:             ?assert(not maps:is_key(<<"alert">>, Body)),
  313:             ?assertMatch(#{<<"data">> := #{<<"message-count">> := 876}}, Body),
  314:             ?assertMatch(#{<<"data">> := #{<<"last-message-sender">> := <<"senderId">>}}, Body),
  315:             ?assertMatch(#{<<"data">> := #{<<"last-message-body">> := <<"message body 576364!!">>}}, Body)
  316: 
  317:         end).
  318: 
  319: %%--------------------------------------------------------------------
  320: %% Test helpers
  321: %%--------------------------------------------------------------------
  322: 
  323: send_notification(User, Node, Notification, Options) ->
  324:     PublishIQ = publish_iq(User, Node, Notification, Options),
  325:     escalus:send(User, PublishIQ),
  326:     escalus:assert(is_iq_result, escalus:wait_for_stanza(User)).
  327: 
  328: get_mocked_request() ->
  329:     {Req, BodyRaw} = next_rest_req(),
  330:     Body = jiffy:decode(BodyRaw, [return_maps]),
  331:     {Req, Body}.
  332: 
  333: prepare_notification() ->
  334:     prepare_notification([]).
  335: prepare_notification(CustomOptions) ->
  336:     Notification = [
  337:         {<<"message-count">>, <<"876">>},
  338:         {<<"last-message-sender">>, <<"senderId">>},
  339:         {<<"last-message-body">>, <<"message body 576364!!">>}
  340:     ],
  341: 
  342:     Options = [
  343:         {<<"device_id">>, <<"sometoken">>},
  344:         {<<"service">>, <<"some_awesome_service">>},
  345:         {<<"mode">>, <<"selected_mode">>},
  346:         {<<"topic">>, <<"some_topic">>}
  347:     ],
  348: 
  349:     {Notification, Options ++ CustomOptions}.
  350: 
  351: setup_pubsub(User) ->
  352:     Node = push_pubsub_node(),
  353:     pubsub_tools:create_node(User, Node, [{type, <<"push">>}]),
  354:     Node.
  355: 
  356: %% ----------------------------------
  357: %% Stanzas
  358: %% ----------------------------------
  359: 
  360: publish_iq(Client, Node, Content, Options) ->
  361:     Item =
  362:         #xmlel{name = <<"notification">>,
  363:                attrs = [{<<"xmlns">>, ?NS_PUSH}],
  364:                children = push_helper:maybe_form(Content, ?PUSH_FORM_TYPE)},
  365:     OptionsEl =
  366:         #xmlel{name = <<"publish-options">>,
  367:                children = push_helper:maybe_form(Options, ?NS_PUBSUB_PUB_OPTIONS)},
  368: 
  369:     Publish = escalus_pubsub_stanza:publish(Client, <<"itemid">>, Item, <<"id">>, Node),
  370:     #xmlel{children = [#xmlel{} = PubsubEl]} = Publish,
  371:     NewPubsubEl = PubsubEl#xmlel{children = PubsubEl#xmlel.children ++ [OptionsEl]},
  372:     Publish#xmlel{children = [NewPubsubEl]}.
  373: 
  374: 
  375: %% ----------------------------------
  376: %% Other helpers
  377: %% ----------------------------------
  378: 
  379: push_pubsub_node() ->
  380:     pubsub_tools:pubsub_node_with_subdomain(?PUBSUB_SUB_DOMAIN ++ ".").
  381: 
  382: parse_form(#xmlel{name = <<"x">>} = Form) ->
  383:     parse_form(exml_query:subelements(Form, <<"field">>));
  384: parse_form(Fields) when is_list(Fields) ->
  385:     lists:map(
  386:         fun(Field) ->
  387:             {exml_query:attr(Field, <<"var">>),
  388:              exml_query:path(Field, [{element, <<"value">>}, cdata])}
  389:         end, Fields).
  390: 
  391: -spec rpc(M :: atom(), F :: atom(), A :: [term()]) -> term().
  392: rpc(M, F, A) ->
  393:     Node = ct:get_config({hosts, mim, node}),
  394:     Cookie = escalus_ct:get_config(ejabberd_cookie),
  395:     escalus_rpc:call(Node, M, F, A, 10000, Cookie).
  396: 
  397: bare_jid(JIDOrClient) ->
  398:     ShortJID = escalus_client:short_jid(JIDOrClient),
  399:     list_to_binary(string:to_lower(binary_to_list(ShortJID))).
  400: 
  401: %% ----------------------------------------------
  402: %% REST mock handler
  403: setup_mock_rest() ->
  404:     TestPid = self(),
  405:     HandleFun = fun(Req) -> handle(Req, TestPid) end,
  406:     {ok, _} = http_helper:start(0, "/[:level1/[:level2/[:level3/[:level4]]]]",
  407:                                           HandleFun),
  408:     http_helper:port().
  409: 
  410: handle(Req, Master) ->
  411:     {ok, Body, Req2} = cowboy_req:read_body(Req),
  412:     Master ! {rest_req, Req2, Body},
  413:     cowboy_req:reply(204, #{}, <<>>, Req).
  414: 
  415: teardown_mock_rest() ->
  416:     http_helper:stop().
  417: 
  418: next_rest_req() ->
  419:     receive
  420:         {rest_req, Req, Body} ->
  421:             {Req, Body}
  422:     after timer:seconds(5) ->
  423:         throw(rest_mock_timeout)
  424:     end.
  425: 
  426: pubsub_host(Host) ->
  427:     ?PUBSUB_SUB_DOMAIN ++ "." ++ Host.
  428: 
  429: %% Module config
  430: required_modules(APIVersion) ->
  431:     [{mod_pubsub, config_parser_helper:mod_config(mod_pubsub, #{
  432:         plugins => [<<"dag">>, <<"push">>],
  433:         nodetree => nodetree_dag,
  434:         host => subhost_pattern(?PUBSUB_SUB_DOMAIN ++ ".@HOST@"),
  435:         backend => mongoose_helper:mnesia_or_rdbms_backend()
  436:     })},
  437:      {mod_push_service_mongoosepush,
  438:       config_parser_helper:mod_config(mod_push_service_mongoosepush, #{pool_name => mongoose_push_http,
  439:                                                                        api_version => APIVersion})
  440:      }].
  441: 
  442: restart_modules(Config, APIVersion) ->
  443:     dynamic_modules:restore_modules(Config),
  444:     dynamic_modules:ensure_modules(domain(), required_modules(APIVersion)),
  445:     Config.