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.