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.