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, [ 434: {plugins, [<<"dag">>, <<"push">>]}, 435: {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.