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 = #{server => "http://localhost:" ++ integer_to_list(MongoosePushMockPort),
   82:                  path_prefix => "/",
   83:                  request_timeout => 2000},
   84:     PoolOpts = #{strategy => available_worker, workers => 20},
   85:     rpc(mongoose_wpool, start_configured_pools,
   86:         [[#{type => http, scope => global, tag => mongoose_push_http,
   87:             opts => PoolOpts, conn_opts => HTTPOpts}]]),
   88:     escalus:init_per_testcase(CaseName, Config).
   89: 
   90: 
   91: end_per_testcase(CaseName, Config) ->
   92:     rpc(mongoose_wpool, stop, [http, global, mongoose_push_http]),
   93:     teardown_mock_rest(),
   94:     escalus:end_per_testcase(CaseName, Config).
   95: 
   96: %%--------------------------------------------------------------------
   97: %% GROUP disco
   98: %%--------------------------------------------------------------------
   99: has_disco_identity(Config) ->
  100:     escalus:story(
  101:         Config, [{alice, 1}],
  102:         fun(Alice) ->
  103:             Server = pubsub_tools:node_addr(?PUBSUB_SUB_DOMAIN ++ "."),
  104:             escalus:send(Alice, escalus_stanza:disco_info(Server)),
  105:             Stanza = escalus:wait_for_stanza(Alice),
  106:             escalus:assert(has_identity, [<<"pubsub">>, <<"push">>], Stanza)
  107:         end).
  108: 
  109: %%--------------------------------------------------------------------
  110: %% GROUP allocate
  111: %%--------------------------------------------------------------------
  112: 
  113: allocate_basic_node(Config) ->
  114:     escalus:story(
  115:         Config, [{alice, 1}],
  116:         fun(Alice) ->
  117:             Node = push_pubsub_node(),
  118:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}])
  119:         end).
  120: 
  121: %%--------------------------------------------------------------------
  122: %% GROUP pubsub_publish
  123: %%--------------------------------------------------------------------
  124: 
  125: publish_fails_with_invalid_item(Config) ->
  126:     escalus:story(
  127:         Config, [{alice, 1}],
  128:         fun(Alice) ->
  129:             Node = push_pubsub_node(),
  130:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}]),
  131: 
  132:             Item =
  133:                 #xmlel{name = <<"invalid-item">>,
  134:                        attrs = [{<<"xmlns">>, ?NS_PUSH}]},
  135: 
  136:             Publish = escalus_pubsub_stanza:publish(Alice, <<"itemid">>, Item, <<"id">>, Node),
  137:             escalus:send(Alice, Publish),
  138:             escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  139:                            escalus:wait_for_stanza(Alice)),
  140: 
  141:             ok
  142: 
  143:         end).
  144: 
  145: publish_fails_with_no_options(Config) ->
  146:     escalus:story(
  147:         Config, [{alice, 1}],
  148:         fun(Alice) ->
  149:             Node = push_pubsub_node(),
  150:             pubsub_tools:create_node(Alice, Node, [{type, <<"push">>}]),
  151: 
  152:             ContentFields = [
  153:                 {<<"FORM_TYPE">>, ?PUSH_FORM_TYPE},
  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:make_form(ContentFields)]},
  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:     ContentFields = [{<<"FORM_TYPE">>, ?PUSH_FORM_TYPE}] ++ Content,
  362:     OptionFileds = [{<<"FORM_TYPE">>, ?NS_PUBSUB_PUB_OPTIONS}] ++ Options,
  363: 
  364:     Item =
  365:         #xmlel{name = <<"notification">>,
  366:                attrs = [{<<"xmlns">>, ?NS_PUSH}],
  367:                children = [push_helper:make_form(ContentFields)]},
  368:     OptionsEl =
  369:         #xmlel{name = <<"publish-options">>, children = [push_helper:make_form(OptionFileds)]},
  370: 
  371:     Publish = escalus_pubsub_stanza:publish(Client, <<"itemid">>, Item, <<"id">>, Node),
  372:     #xmlel{children = [#xmlel{} = PubsubEl]} = Publish,
  373:     NewPubsubEl = PubsubEl#xmlel{children = PubsubEl#xmlel.children ++ [OptionsEl]},
  374:     Publish#xmlel{children = [NewPubsubEl]}.
  375: 
  376: 
  377: %% ----------------------------------
  378: %% Other helpers
  379: %% ----------------------------------
  380: 
  381: push_pubsub_node() ->
  382:     pubsub_tools:pubsub_node_with_subdomain(?PUBSUB_SUB_DOMAIN ++ ".").
  383: 
  384: parse_form(#xmlel{name = <<"x">>} = Form) ->
  385:     parse_form(exml_query:subelements(Form, <<"field">>));
  386: parse_form(Fields) when is_list(Fields) ->
  387:     lists:map(
  388:         fun(Field) ->
  389:             {exml_query:attr(Field, <<"var">>),
  390:              exml_query:path(Field, [{element, <<"value">>}, cdata])}
  391:         end, Fields).
  392: 
  393: -spec rpc(M :: atom(), F :: atom(), A :: [term()]) -> term().
  394: rpc(M, F, A) ->
  395:     Node = ct:get_config({hosts, mim, node}),
  396:     Cookie = escalus_ct:get_config(ejabberd_cookie),
  397:     escalus_rpc:call(Node, M, F, A, 10000, Cookie).
  398: 
  399: bare_jid(JIDOrClient) ->
  400:     ShortJID = escalus_client:short_jid(JIDOrClient),
  401:     list_to_binary(string:to_lower(binary_to_list(ShortJID))).
  402: 
  403: %% ----------------------------------------------
  404: %% REST mock handler
  405: setup_mock_rest() ->
  406:     TestPid = self(),
  407:     HandleFun = fun(Req) -> handle(Req, TestPid) end,
  408:     {ok, _} = http_helper:start(0, "/[:level1/[:level2/[:level3/[:level4]]]]",
  409:                                           HandleFun),
  410:     http_helper:port().
  411: 
  412: handle(Req, Master) ->
  413:     {ok, Body, Req2} = cowboy_req:read_body(Req),
  414:     Master ! {rest_req, Req2, Body},
  415:     cowboy_req:reply(204, #{}, <<>>, Req).
  416: 
  417: teardown_mock_rest() ->
  418:     http_helper:stop().
  419: 
  420: next_rest_req() ->
  421:     receive
  422:         {rest_req, Req, Body} ->
  423:             {Req, Body}
  424:     after timer:seconds(5) ->
  425:         throw(rest_mock_timeout)
  426:     end.
  427: 
  428: pubsub_host(Host) ->
  429:     ?PUBSUB_SUB_DOMAIN ++ "." ++ Host.
  430: 
  431: %% Module config
  432: required_modules(APIVersion) ->
  433:     [{mod_pubsub, config_parser_helper:mod_config(mod_pubsub, #{
  434:         plugins => [<<"dag">>, <<"push">>],
  435:         nodetree => nodetree_dag,
  436:         host => subhost_pattern(?PUBSUB_SUB_DOMAIN ++ ".@HOST@")
  437:     })},
  438:      {mod_push_service_mongoosepush,
  439:       config_parser_helper:mod_config(mod_push_service_mongoosepush, #{pool_name => mongoose_push_http,
  440:                                                                        api_version => APIVersion})
  441:      }].
  442: 
  443: restart_modules(Config, APIVersion) ->
  444:     dynamic_modules:restore_modules(Config),
  445:     dynamic_modules:ensure_modules(domain(), required_modules(APIVersion)),
  446:     Config.