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