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:             Publish = escalus_pubsub_stanza:publish(Alice, <<"itemid">>, Item, <<"id">>, Node),
  132:             escalus:send(Alice, Publish),
  133:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  134:                            escalus:wait_for_stanza(Alice)),
  135: 
  136:             ok
  137: 
  138:         end).
  139: 
  140: publish_fails_with_no_options(Config) ->
  141:     escalus:story(
  142:         Config, [{alice, 1}],
  143:         fun(Alice) ->
  144:             Node = push_pubsub_node(),
  145:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}]),
  146: 
  147:             ContentFields = [
  148:                 {<<"FORM_TYPE">>, ?PUSH_FORM_TYPE},
  149:                 {<<"message-count">>, <<"1">>},
  150:                 {<<"last-message-sender">>, <<"senderId">>},
  151:                 {<<"last-message-body">>, <<"message body">>}
  152:             ],
  153: 
  154:             Item =
  155:                 #xmlel{name = <<"notification">>,
  156:                        attrs = [{<<"xmlns">>, ?NS_PUSH}],
  157:                        children = [push_helper:make_form(ContentFields)]},
  158: 
  159:             Publish = escalus_pubsub_stanza:publish(Alice, <<"itemid">>, Item, <<"id">>, Node),
  160:             escalus:send(Alice, Publish),
  161:             escalus:assert(is_error, [<<"cancel">>, <<"conflict">>],
  162:                            escalus:wait_for_stanza(Alice)),
  163: 
  164:             ok
  165: 
  166:         end).
  167: 
  168: publish_succeeds_with_valid_options(Config) ->
  169:     escalus:story(
  170:         Config, [{alice, 1}],
  171:         fun(Alice) ->
  172:             Node = push_pubsub_node(),
  173:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}]),
  174: 
  175:             Content = [
  176:                 {<<"message-count">>, <<"1">>},
  177:                 {<<"last-message-sender">>, <<"senderId">>},
  178:                 {<<"last-message-body">>, <<"message body">>}
  179:             ],
  180: 
  181:             Options = [
  182:                 {<<"device_id">>, <<"sometoken">>},
  183:                 {<<"service">>, <<"apns">>}
  184:             ],
  185: 
  186:             PublishIQ = publish_iq(Alice, Node, Content, Options),
  187:             escalus:send(Alice, PublishIQ),
  188:             escalus:assert(is_iq_result, escalus:wait_for_stanza(Alice)),
  189: 
  190:             ok
  191: 
  192:         end).
  193: 
  194: push_node_can_be_configured_to_whitelist_publishers(Config) ->
  195:     escalus:story(
  196:         Config, [{alice, 1}, {bob, 1}],
  197:         fun(Alice, Bob) ->
  198:             Node = push_pubsub_node(),
  199:             Configuration = [{<<"pubsub#access_model">>, <<"whitelist">>},
  200:                              {<<"pubsub#publish_model">>, <<"publishers">>}],
  201:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>},
  202:                                                    {config, Configuration}]),
  203: 
  204:             ActiveConfig = pubsub_tools:get_configuration(Alice, Node, []),
  205:             ?assertMatch({_, _, <<"whitelist">>}, lists:keyfind(<<"pubsub#access_model">>, 1, ActiveConfig)),
  206:             ?assertMatch({_, _, <<"publishers">>}, lists:keyfind(<<"pubsub#publish_model">>, 1, ActiveConfig)),
  207: 
  208:             Content = [
  209:                 {<<"message-count">>, <<"1">>},
  210:                 {<<"last-message-sender">>, <<"senderId">>},
  211:                 {<<"last-message-body">>, <<"message body">>}
  212:             ],
  213: 
  214:             Options = [
  215:                 {<<"device_id">>, <<"sometoken">>},
  216:                 {<<"service">>, <<"apns">>}
  217:             ],
  218: 
  219:             PublishIQ = publish_iq(Bob, Node, Content, Options),
  220:             escalus:send(Bob, PublishIQ),
  221:             escalus:assert(is_error, [<<"auth">>, <<"forbidden">>],
  222:                            escalus:wait_for_stanza(Bob)),
  223: 
  224: 
  225:             ok
  226: 
  227:         end).
  228: 
  229: %%--------------------------------------------------------------------
  230: %% GROUP rest_integration
  231: %%--------------------------------------------------------------------
  232: 
  233: 
  234: rest_service_called_with_correct_path(Version, Config) ->
  235:     escalus:story(
  236:         Config, [{alice, 1}],
  237:         fun(Alice) ->
  238:             Node = setup_pubsub(Alice),
  239:             {Notification, Options} = prepare_notification(),
  240:             send_notification(Alice, Node, Notification, Options),
  241:             {Req, _Body} = get_mocked_request(),
  242: 
  243:             ?assertMatch(<<"POST">>, cowboy_req:method(Req)),
  244:             ?assertMatch(Version, cowboy_req:binding(level1, Req)),
  245:             ?assertMatch(<<"notification">>, cowboy_req:binding(level2, Req)),
  246:             ?assertMatch(<<"sometoken">>, cowboy_req:binding(level3, Req)),
  247:             ?assertMatch(undefined, cowboy_req:binding(level4, Req))
  248:         end).
  249: 
  250: rest_service_called_with_correct_path_v1(Config) ->
  251:     rest_service_called_with_correct_path(<<"v1">>, Config).
  252: 
  253: rest_service_called_with_correct_path_v2(Config) ->
  254:     rest_service_called_with_correct_path(<<"v2">>, Config).
  255: 
  256: 
  257: rest_service_gets_correct_payload_v1(Config) ->
  258:     escalus:story(
  259:         Config, [{alice, 1}],
  260:         fun(Alice) ->
  261:             Node = setup_pubsub(Alice),
  262:             {Notification, Options} = prepare_notification(),
  263:             send_notification(Alice, Node, Notification, Options),
  264:             {_, Body} = get_mocked_request(),
  265: 
  266:             ?assertMatch(#{<<"service">> := <<"some_awesome_service">>}, Body),
  267:             ?assertMatch(#{<<"badge">> := 876}, Body),
  268:             ?assertMatch(#{<<"title">> := <<"senderId">>}, Body),
  269:             ?assertMatch(#{<<"tag">> := <<"senderId">>}, Body),
  270:             ?assertMatch(#{<<"mode">> := <<"selected_mode">>}, Body),
  271:             ?assertMatch(#{<<"body">> := <<"message body 576364!!">>}, Body)
  272:         end).
  273: 
  274: rest_service_gets_correct_payload_v2(Config) ->
  275:     escalus:story(
  276:         Config, [{alice, 1}],
  277:         fun(Alice) ->
  278:             Node = setup_pubsub(Alice),
  279:             {Notification, Options} = prepare_notification(),
  280:             send_notification(Alice, Node, Notification, Options),
  281:             {_, Body} = get_mocked_request(),
  282: 
  283: 
  284:             ?assertMatch(#{<<"service">> := <<"some_awesome_service">>}, Body),
  285:             ?assertMatch(#{<<"mode">> := <<"selected_mode">>}, Body),
  286:             ?assertMatch(#{<<"topic">> := <<"some_topic">>}, Body),
  287:             ?assert(not maps:is_key(<<"data">>, Body)),
  288:             ?assertMatch(#{<<"alert">> := #{<<"badge">> := 876}}, Body),
  289:             ?assertMatch(#{<<"alert">> := #{<<"title">> := <<"senderId">>}}, Body),
  290:             ?assertMatch(#{<<"alert">> := #{<<"tag">> := <<"senderId">>}}, Body),
  291:             ?assertMatch(#{<<"alert">> := #{<<"body">> := <<"message body 576364!!">>}}, Body)
  292: 
  293:         end).
  294: 
  295: rest_service_gets_correct_payload_silent_v2(Config) ->
  296:     escalus:story(
  297:         Config, [{alice, 1}],
  298:         fun(Alice) ->
  299:             Node = setup_pubsub(Alice),
  300:             {Notification, Options} = prepare_notification([{<<"silent">>, <<"true">>}]),
  301:             send_notification(Alice, Node, Notification, Options),
  302:             {_, Body} = get_mocked_request(),
  303: 
  304:             ?assertMatch(#{<<"service">> := <<"some_awesome_service">>}, Body),
  305:             ?assertMatch(#{<<"mode">> := <<"selected_mode">>}, Body),
  306:             ?assertMatch(#{<<"topic">> := <<"some_topic">>}, Body),
  307:             ?assert(not maps:is_key(<<"alert">>, Body)),
  308:             ?assertMatch(#{<<"data">> := #{<<"message-count">> := 876}}, Body),
  309:             ?assertMatch(#{<<"data">> := #{<<"last-message-sender">> := <<"senderId">>}}, Body),
  310:             ?assertMatch(#{<<"data">> := #{<<"last-message-body">> := <<"message body 576364!!">>}}, Body)
  311: 
  312:         end).
  313: 
  314: %%--------------------------------------------------------------------
  315: %% Test helpers
  316: %%--------------------------------------------------------------------
  317: 
  318: send_notification(User, Node, Notification, Options) ->
  319:     PublishIQ = publish_iq(User, Node, Notification, Options),
  320:     escalus:send(User, PublishIQ),
  321:     escalus:assert(is_iq_result, escalus:wait_for_stanza(User)).
  322: 
  323: get_mocked_request() ->
  324:     {Req, BodyRaw} = next_rest_req(),
  325:     Body = jiffy:decode(BodyRaw, [return_maps]),
  326:     {Req, Body}.
  327: 
  328: prepare_notification() ->
  329:     prepare_notification([]).
  330: prepare_notification(CustomOptions) ->
  331:     Notification = [
  332:         {<<"message-count">>, <<"876">>},
  333:         {<<"last-message-sender">>, <<"senderId">>},
  334:         {<<"last-message-body">>, <<"message body 576364!!">>}
  335:     ],
  336: 
  337:     Options = [
  338:         {<<"device_id">>, <<"sometoken">>},
  339:         {<<"service">>, <<"some_awesome_service">>},
  340:         {<<"mode">>, <<"selected_mode">>},
  341:         {<<"topic">>, <<"some_topic">>}
  342:     ],
  343: 
  344:     {Notification, Options ++ CustomOptions}.
  345: 
  346: setup_pubsub(User) ->
  347:     Node = push_pubsub_node(),
  348:     pubsub_tools:create_node(User, Node, [{type, <<"push">>}]),
  349:     Node.
  350: 
  351: %% ----------------------------------
  352: %% Stanzas
  353: %% ----------------------------------
  354: 
  355: publish_iq(Client, Node, Content, Options) ->
  356:     ContentFields = [{<<"FORM_TYPE">>, ?PUSH_FORM_TYPE}] ++ Content,
  357:     OptionFileds = [{<<"FORM_TYPE">>, ?NS_PUBSUB_PUB_OPTIONS}] ++ Options,
  358: 
  359:     Item =
  360:         #xmlel{name = <<"notification">>,
  361:                attrs = [{<<"xmlns">>, ?NS_PUSH}],
  362:                children = [push_helper:make_form(ContentFields)]},
  363:     OptionsEl =
  364:         #xmlel{name = <<"publish-options">>, children = [push_helper:make_form(OptionFileds)]},
  365: 
  366:     Publish = escalus_pubsub_stanza:publish(Client, <<"itemid">>, Item, <<"id">>, Node),
  367:     #xmlel{children = [#xmlel{} = PubsubEl]} = Publish,
  368:     NewPubsubEl = PubsubEl#xmlel{children = PubsubEl#xmlel.children ++ [OptionsEl]},
  369:     Publish#xmlel{children = [NewPubsubEl]}.
  370: 
  371: 
  372: %% ----------------------------------
  373: %% Other helpers
  374: %% ----------------------------------
  375: 
  376: push_pubsub_node() ->
  377:     pubsub_tools:pubsub_node_with_subdomain(?PUBSUB_SUB_DOMAIN ++ ".").
  378: 
  379: parse_form(#xmlel{name = <<"x">>} = Form) ->
  380:     parse_form(exml_query:subelements(Form, <<"field">>));
  381: parse_form(Fields) when is_list(Fields) ->
  382:     lists:map(
  383:         fun(Field) ->
  384:             {exml_query:attr(Field, <<"var">>),
  385:              exml_query:path(Field, [{element, <<"value">>}, cdata])}
  386:         end, Fields).
  387: 
  388: -spec rpc(M :: atom(), F :: atom(), A :: [term()]) -> term().
  389: rpc(M, F, A) ->
  390:     Node = ct:get_config({hosts, mim, node}),
  391:     Cookie = escalus_ct:get_config(ejabberd_cookie),
  392:     escalus_rpc:call(Node, M, F, A, 10000, Cookie).
  393: 
  394: bare_jid(JIDOrClient) ->
  395:     ShortJID = escalus_client:short_jid(JIDOrClient),
  396:     list_to_binary(string:to_lower(binary_to_list(ShortJID))).
  397: 
  398: %% ----------------------------------------------
  399: %% REST mock handler
  400: setup_mock_rest() ->
  401:     TestPid = self(),
  402:     HandleFun = fun(Req) -> handle(Req, TestPid) end,
  403:     {ok, _} = http_helper:start(0, "/[:level1/[:level2/[:level3/[:level4]]]]",
  404:                                           HandleFun),
  405:     http_helper:port().
  406: 
  407: handle(Req, Master) ->
  408:     {ok, Body, Req2} = cowboy_req:read_body(Req),
  409:     Master ! {rest_req, Req2, Body},
  410:     cowboy_req:reply(204, #{}, <<>>, Req).
  411: 
  412: teardown_mock_rest() ->
  413:     http_helper:stop().
  414: 
  415: next_rest_req() ->
  416:     receive
  417:         {rest_req, Req, Body} ->
  418:             {Req, Body}
  419:     after timer:seconds(5) ->
  420:         throw(rest_mock_timeout)
  421:     end.
  422: 
  423: pubsub_host(Host) ->
  424:     ?PUBSUB_SUB_DOMAIN ++ "." ++ Host.
  425: 
  426: %% Module config
  427: required_modules(APIVersion) ->
  428:     [{mod_pubsub, config_parser_helper:mod_config(mod_pubsub, #{
  429:         plugins => [<<"dag">>, <<"push">>],
  430:         nodetree => nodetree_dag,
  431:         host => subhost_pattern(?PUBSUB_SUB_DOMAIN ++ ".@HOST@")
  432:     })},
  433:      {mod_push_service_mongoosepush,
  434:       config_parser_helper:mod_config(mod_push_service_mongoosepush, #{pool_name => mongoose_push_http,
  435:                                                                        api_version => APIVersion})
  436:      }].
  437: 
  438: restart_modules(Config, APIVersion) ->
  439:     dynamic_modules:restore_modules(Config),
  440:     dynamic_modules:ensure_modules(domain(), required_modules(APIVersion)),
  441:     Config.