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.