1: %%%=================================================================== 2: %%% @copyright (C) 2016, Erlang Solutions Ltd. 3: %%% @doc Suite for testing Personal Eventing Protocol features 4: %%% as described in XEP-0163 5: %%% @end 6: %%%=================================================================== 7: 8: -module(pep_SUITE). 9: 10: -include_lib("escalus/include/escalus.hrl"). 11: -include_lib("common_test/include/ct.hrl"). 12: -include_lib("escalus/include/escalus_xmlns.hrl"). 13: -include_lib("exml/include/exml.hrl"). 14: -include_lib("exml/include/exml_stream.hrl"). 15: -include_lib("eunit/include/eunit.hrl"). 16: 17: -export([suite/0, all/0, groups/0]). 18: -export([init_per_suite/1, end_per_suite/1, 19: init_per_group/2, end_per_group/2, 20: init_per_testcase/2, end_per_testcase/2]). 21: 22: -export([ 23: disco_test/1, 24: disco_sm_test/1, 25: disco_sm_items_test/1, 26: pep_caps_test/1, 27: publish_and_notify_test/1, 28: publish_options_test/1, 29: send_caps_after_login_test/1, 30: delayed_receive/1, 31: delayed_receive_with_sm/1, 32: h_ok_after_notify_test/1, 33: authorize_access_model/1, 34: unsubscribe_after_presence_unsubscription/1 35: ]). 36: 37: -export([ 38: send_initial_presence_with_caps/2 39: ]). 40: 41: -import(distributed_helper, [mim/0, 42: require_rpc_nodes/1, 43: subhost_pattern/1, 44: rpc/4]). 45: 46: -import(domain_helper, [domain/0]). 47: 48: %%-------------------------------------------------------------------- 49: %% Suite configuration 50: %%-------------------------------------------------------------------- 51: 52: all() -> 53: [ 54: {group, pep_tests} 55: ]. 56: 57: groups() -> 58: G = [ 59: {pep_tests, [parallel], 60: [ 61: disco_test, 62: disco_sm_test, 63: disco_sm_items_test, 64: pep_caps_test, 65: publish_and_notify_test, 66: publish_options_test, 67: send_caps_after_login_test, 68: delayed_receive, 69: delayed_receive_with_sm, 70: h_ok_after_notify_test, 71: authorize_access_model, 72: unsubscribe_after_presence_unsubscription 73: ] 74: }, 75: {cache_tests, [parallel], 76: [ 77: send_caps_after_login_test, 78: delayed_receive, 79: delayed_receive_with_sm, 80: unsubscribe_after_presence_unsubscription 81: ] 82: } 83: ], 84: ct_helper:repeat_all_until_all_ok(G). 85: 86: suite() -> 87: require_rpc_nodes([mim]) ++ escalus:suite(). 88: 89: %%-------------------------------------------------------------------- 90: %% Init & teardown 91: %%-------------------------------------------------------------------- 92: 93: init_per_suite(Config) -> 94: escalus:init_per_suite(dynamic_modules:save_modules(domain(), Config)). 95: 96: end_per_suite(Config) -> 97: escalus_fresh:clean(), 98: dynamic_modules:restore_modules(Config), 99: escalus:end_per_suite(Config). 100: 101: init_per_group(cache_tests, Config) -> 102: Config0 = dynamic_modules:save_modules(domain(), Config), 103: NewConfig = required_modules(cache_tests), 104: dynamic_modules:ensure_modules(domain(), NewConfig), 105: Config0; 106: 107: init_per_group(_GroupName, Config) -> 108: dynamic_modules:ensure_modules(domain(), required_modules()), 109: Config. 110: 111: end_per_group(cache_tests, Config) -> 112: dynamic_modules:restore_modules(Config); 113: 114: end_per_group(_GroupName, Config) -> 115: Config. 116: 117: init_per_testcase(TestName, Config) -> 118: escalus:init_per_testcase(TestName, Config). 119: 120: end_per_testcase(TestName, Config) -> 121: escalus:end_per_testcase(TestName, Config). 122: 123: %%-------------------------------------------------------------------- 124: %% Test cases for XEP-0163 125: %% Comments in test cases refer to sections is the XEP 126: %%-------------------------------------------------------------------- 127: 128: %% Group: pep_tests (sequence) 129: 130: disco_test(Config) -> 131: escalus:fresh_story( 132: Config, 133: [{alice, 1}], 134: fun(Alice) -> 135: escalus:send(Alice, escalus_stanza:disco_info(pubsub_tools:node_addr())), 136: Stanza = escalus:wait_for_stanza(Alice), 137: escalus:assert(has_identity, [<<"pubsub">>, <<"service">>], Stanza), 138: escalus:assert(has_identity, [<<"pubsub">>, <<"pep">>], Stanza), 139: escalus:assert(has_feature, [?NS_PUBSUB], Stanza) 140: end). 141: 142: disco_sm_test(Config) -> 143: escalus:fresh_story( 144: Config, 145: [{alice, 1}], 146: fun(Alice) -> 147: AliceJid = escalus_client:short_jid(Alice), 148: escalus:send(Alice, escalus_stanza:disco_info(AliceJid)), 149: Stanza = escalus:wait_for_stanza(Alice), 150: ?assertNot(escalus_pred:has_identity(<<"pubsub">>, <<"service">>, Stanza)), 151: escalus:assert(has_identity, [<<"pubsub">>, <<"pep">>], Stanza), 152: escalus:assert(has_feature, [?NS_PUBSUB], Stanza), 153: escalus:assert(is_stanza_from, [AliceJid], Stanza) 154: end). 155: 156: disco_sm_items_test(Config) -> 157: NodeNS = random_node_ns(), 158: escalus:fresh_story( 159: set_caps_node(NodeNS, Config), 160: [{alice, 1}], 161: fun(Alice) -> 162: AliceJid = escalus_client:short_jid(Alice), 163: 164: %% Node not present yet 165: escalus:send(Alice, escalus_stanza:disco_items(AliceJid)), 166: Stanza1 = escalus:wait_for_stanza(Alice), 167: Query1 = exml_query:subelement(Stanza1, <<"query">>), 168: ?assertEqual(undefined, exml_query:subelement_with_attr(Query1, <<"node">>, NodeNS)), 169: escalus:assert(is_stanza_from, [AliceJid], Stanza1), 170: 171: %% Publish an item to trigger node creation 172: pubsub_tools:publish(Alice, <<"item1">>, {pep, NodeNS}, []), 173: 174: %% Node present 175: escalus:send(Alice, escalus_stanza:disco_items(AliceJid)), 176: Stanza2 = escalus:wait_for_stanza(Alice), 177: Query2 = exml_query:subelement(Stanza2, <<"query">>), 178: Item = exml_query:subelement_with_attr(Query2, <<"node">>, NodeNS), 179: ?assertEqual(jid:str_tolower(AliceJid), exml_query:attr(Item, <<"jid">>)), 180: escalus:assert(is_stanza_from, [AliceJid], Stanza2) 181: end). 182: 183: pep_caps_test(Config) -> 184: escalus:fresh_story( 185: Config, 186: [{bob, 1}], 187: fun(Bob) -> 188: NodeNS = random_node_ns(), 189: Caps = caps(NodeNS), 190: 191: %% Send presence with capabilities (chap. 1 ex. 4) 192: %% Server does not know the version string, so it requests feature list 193: send_presence_with_caps(Bob, Caps), 194: DiscoRequest = escalus:wait_for_stanza(Bob), 195: 196: %% Client responds with a list of supported features (chap. 1 ex. 5) 197: send_caps_disco_result(Bob, DiscoRequest, NodeNS), 198: 199: receive_presence_with_caps(Bob, Bob, Caps) 200: end). 201: 202: publish_and_notify_test(Config) -> 203: NodeNS = random_node_ns(), 204: escalus:fresh_story( 205: set_caps_node(NodeNS, Config), 206: [{alice, 1}, {bob, 1}], 207: fun(Alice, Bob) -> 208: escalus_story:make_all_clients_friends([Alice, Bob]), 209: 210: pubsub_tools:publish(Alice, <<"item1">>, {pep, NodeNS}, []), 211: pubsub_tools:receive_item_notification( 212: Bob, <<"item1">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []) 213: end). 214: 215: set_caps_node(NodeNS, Config) -> 216: [{escalus_overrides, 217: [{initial_activity, {?MODULE, send_initial_presence_with_caps, [NodeNS]}}]} 218: | Config]. 219: 220: publish_options_test(Config) -> 221: % Given pubsub is configured with pep plugin 222: escalus:fresh_story( 223: Config, 224: [{alice, 1}], 225: fun(Alice) -> 226: 227: % When publishing an item with publish-options 228: Node = {pep, random_node_ns()}, 229: PublishOptions = [{<<"pubsub#access_model">>, <<"open">>}], 230: pubsub_tools:publish_with_options(Alice, <<"item1">>, Node, [], PublishOptions), 231: 232: % Then node configuration contains specified publish-options 233: NodeConfig = pubsub_tools:get_configuration(Alice, Node, []), 234: verify_publish_options(NodeConfig, PublishOptions) 235: end). 236: 237: send_caps_after_login_test(Config) -> 238: escalus:fresh_story( 239: Config, 240: [{alice, 1}, {bob, 1}], 241: fun(Alice, Bob) -> 242: NodeNS = random_node_ns(), 243: pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []), 244: 245: escalus_story:make_all_clients_friends([Alice, Bob]), 246: 247: %% Presence subscription triggers PEP last item sending 248: %% and sometimes this async process takes place after caps 249: %% are updated, leading to duplicated notification 250: %% We use timer:sleep here to avoid it for now, because 251: %% TODO: mod_pubsub send loop has to be fixed, supervised, refactored etc. 252: timer:sleep(1000), 253: 254: Caps = caps(NodeNS), 255: send_presence_with_caps_and_handle_disco(Bob, Caps, NodeNS), 256: receive_presence_with_caps(Bob, Bob, Caps), 257: receive_presence_with_caps(Alice, Bob, Caps), 258: 259: pubsub_tools:receive_item_notification( 260: Bob, <<"item2">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []), 261: 262: [] = escalus_client:peek_stanzas(Bob) 263: end). 264: 265: delayed_receive(Config) -> 266: %% if alice publishes an item and then bob subscribes successfully to her presence 267: %% then bob will receive the item right after final subscription stanzas 268: NodeNS = random_node_ns(), 269: escalus:fresh_story( 270: [{escalus_overrides, 271: [{initial_activity, {?MODULE, send_initial_presence_with_caps, [NodeNS]}}]} 272: | Config], 273: [{alice, 1}, {bob, 1}], 274: fun(Alice, Bob) -> 275: pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []), 276: [Message] = make_friends(Bob, Alice), 277: ct:pal("Message: ~p", [Message]), 278: pubsub_tools:check_item_notification( 279: Message, <<"item2">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []), 280: ok 281: end). 282: 283: delayed_receive_with_sm(Config) -> 284: %% Same as delayed_receive but with stream management turned on 285: NodeNS = random_node_ns(), 286: escalus:fresh_story( 287: [{escalus_overrides, 288: [{initial_activity, {?MODULE, send_initial_presence_with_caps, [NodeNS]}}]} 289: | Config], 290: [{alice, 1}, {bob, 1}], 291: fun(Alice, Bob) -> 292: enable_sm(Alice), 293: enable_sm(Bob), 294: publish_with_sm(Alice, <<"item2">>, {pep, NodeNS}, []), 295: [Message] = make_friends(Bob, Alice), 296: ct:pal("Message: ~p", [Message]), 297: pubsub_tools:check_item_notification(Message, 298: <<"item2">>, 299: {escalus_utils:get_short_jid(Alice), NodeNS}, 300: []), 301: ok 302: end). 303: 304: h_ok_after_notify_test(ConfigIn) -> 305: Config = escalus_users:update_userspec(ConfigIn, kate, 306: stream_management, true), 307: NodeNS = random_node_ns(), 308: escalus:fresh_story( 309: [{escalus_overrides, 310: [{initial_activity, {?MODULE, send_initial_presence_with_caps, [NodeNS]}}]} | Config ], 311: [{alice, 1}, {kate, 1}], 312: fun(Alice, Kate) -> 313: escalus_story:make_all_clients_friends([Alice, Kate]), 314: 315: %% TODO: Dirty fix. For some reason PEP resends item2 with <delay> element, 316: %% so probably there is some race condition that applies to becoming friends 317: %% and publishing 318: timer:sleep(1000), 319: 320: pubsub_tools:publish(Alice, <<"item2">>, {pep, NodeNS}, []), 321: pubsub_tools:receive_item_notification( 322: Kate, <<"item2">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []), 323: 324: H = escalus_tcp:get_sm_h(Kate#client.rcv_pid), 325: escalus:send(Kate, escalus_stanza:sm_ack(H)), 326: 327: escalus_connection:send(Kate, escalus_stanza:sm_request()), 328: escalus:assert(is_sm_ack, 329: escalus_connection:get_stanza(Kate, stream_mgmt_ack)) 330: end). 331: 332: authorize_access_model(Config) -> 333: escalus:fresh_story(Config, 334: [{alice, 1}, {bob, 1}], 335: fun(Alice, Bob) -> 336: NodeNS = random_node_ns(), 337: {NodeAddr, _} = PepNode = make_pep_node_info(Alice, NodeNS), 338: AccessModel = {<<"pubsub#access_model">>, <<"authorize">>}, 339: pubsub_tools:create_node(Alice, PepNode, [{config, [AccessModel]}]), 340: 341: pubsub_tools:subscribe(Bob, PepNode, [{subscription, <<"pending">>}]), 342: BobsRequest = pubsub_tools:receive_subscription_request(Alice, Bob, PepNode, []), 343: 344: %% FIXME: Only one item should be here but node_pep is based on node_flat, which 345: %% is node_dag's ancestor, so this entry gets duplicated because every plugin 346: %% is queried for subscriptions. Nasty fix involves deduplicating entries 347: %% in mod_pubsub:get_subscriptions. The proper fix means not hacking node plugins 348: %% into serving PEP but it's definitely a major change... 349: Subs = [{NodeNS, <<"pending">>}, {NodeNS, <<"pending">>}], 350: pubsub_tools:get_user_subscriptions(Bob, NodeAddr, [{expected_result, Subs}]), 351: 352: pubsub_tools:submit_subscription_response(Alice, BobsRequest, PepNode, true, []), 353: pubsub_tools:receive_subscription_notification(Bob, <<"subscribed">>, PepNode, []), 354: 355: pubsub_tools:publish(Alice, <<"fish">>, {pep, NodeNS}, []), 356: pubsub_tools:receive_item_notification( 357: Bob, <<"fish">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []), 358: 359: pubsub_tools:delete_node(Alice, PepNode, []) 360: end). 361: 362: unsubscribe_after_presence_unsubscription(Config) -> 363: escalus:fresh_story(Config, 364: [{alice, 1}, {bob, 1}], 365: fun(Alice, Bob) -> 366: escalus_story:make_all_clients_friends([Alice, Bob]), 367: 368: NodeNS = random_node_ns(), 369: PepNode = make_pep_node_info(Alice, NodeNS), 370: pubsub_tools:create_node(Alice, PepNode, []), 371: pubsub_tools:subscribe(Bob, PepNode, []), 372: pubsub_tools:publish(Alice, <<"fish">>, {pep, NodeNS}, []), 373: pubsub_tools:receive_item_notification( 374: Bob, <<"fish">>, {escalus_utils:get_short_jid(Alice), NodeNS}, []), 375: 376: BobJid = escalus_utils:get_short_jid(Bob), 377: escalus:send(Alice, escalus_stanza:presence_direct(BobJid, <<"unsubscribed">>)), 378: %% Bob & Alice get roster update, Bob gets presence unsubscribed & unavailable 379: [_, _, _] = escalus:wait_for_stanzas(Bob, 3), 380: _ = escalus:wait_for_stanza(Alice), 381: 382: %% Unsubscription from PEP nodes is implicit 383: pubsub_tools:publish(Alice, <<"salmon">>, {pep, NodeNS}, []), 384: [] = escalus:wait_for_stanzas(Bob, 1), 385: 386: pubsub_tools:delete_node(Alice, PepNode, []) 387: end). 388: 389: %%----------------------------------------------------------------- 390: %% Helpers 391: %%----------------------------------------------------------------- 392: 393: required_modules() -> 394: [{mod_caps, []}, 395: {mod_pubsub, [ 396: {plugins, [<<"dag">>, <<"pep">>]}, 397: {nodetree, <<"dag">>}, 398: {backend, mongoose_helper:mnesia_or_rdbms_backend()}, 399: {pep_mapping, []}, 400: {host, subhost_pattern("pubsub.@HOST@")} 401: ]}]. 402: required_modules(cache_tests) -> 403: [{mod_caps, []}, 404: {mod_pubsub, [ 405: {plugins, [<<"dag">>, <<"pep">>]}, 406: {nodetree, <<"dag">>}, 407: {backend, mongoose_helper:mnesia_or_rdbms_backend()}, 408: {pep_mapping, []}, 409: {host, subhost_pattern("pubsub.@HOST@")}, 410: {last_item_cache, mongoose_helper:mnesia_or_rdbms_backend()} 411: ]}]. 412: 413: send_initial_presence_with_caps(NodeNS, User) -> 414: case string:to_lower(binary_to_list(escalus_client:username(User))) of 415: "alice" ++ _ -> escalus_story:send_initial_presence(User); 416: "bob" ++ _ -> send_presence_with_caps_and_handle_disco(User, caps(NodeNS), NodeNS); 417: "kate" ++ _ -> send_presence_with_caps_and_handle_disco(User, caps(NodeNS), NodeNS) 418: end. 419: 420: send_presence_with_caps_and_handle_disco(User, Caps, NodeNS) -> 421: send_presence_with_caps(User, Caps), 422: DiscoRequest = escalus:wait_for_stanza(User), 423: send_caps_disco_result(User, DiscoRequest, NodeNS). 424: 425: send_presence_with_caps(User, Caps) -> 426: Presence = escalus_stanza:presence(<<"available">>, [Caps]), 427: escalus:send(User, Presence). 428: 429: send_caps_disco_result(User, DiscoRequest, NodeNS) -> 430: QueryEl = escalus_stanza:query_el(?NS_DISCO_INFO, feature_elems(NodeNS)), 431: DiscoResult = escalus_stanza:iq_result(DiscoRequest, [QueryEl]), 432: escalus:send(User, DiscoResult). 433: 434: receive_presence_with_caps(User1, User2, Caps) -> 435: PresenceNotification = escalus:wait_for_stanza(User1), 436: escalus:assert(is_presence, PresenceNotification), 437: escalus:assert(is_stanza_from, [User2], PresenceNotification), 438: Caps = exml_query:subelement(PresenceNotification, <<"c">>). 439: 440: make_pep_node_info(Client, NodeName) -> 441: {escalus_utils:jid_to_lower(escalus_utils:get_short_jid(Client)), NodeName}. 442: 443: verify_publish_options(FullNodeConfig, Options) -> 444: NodeConfig = [{Opt, Value} || {Opt, _, Value} <- FullNodeConfig], 445: Options = lists:filter(fun(Option) -> 446: lists:member(Option, NodeConfig) 447: end, Options). 448: 449: %%----------------------------------------------------------------- 450: %% XML helpers 451: %%----------------------------------------------------------------- 452: 453: feature_elems(PEPNodeNS) -> 454: [#xmlel{name = <<"identity">>, 455: attrs = [{<<"category">>, <<"client">>}, 456: {<<"name">>, <<"Psi">>}, 457: {<<"type">>, <<"pc">>}]} | 458: [feature_elem(F) || F <- features(PEPNodeNS)]]. 459: 460: feature_elem(F) -> 461: #xmlel{name = <<"feature">>, 462: attrs = [{<<"var">>, F}]}. 463: 464: caps(PEPNodeNS) -> 465: #xmlel{name = <<"c">>, 466: attrs = [{<<"xmlns">>, ?NS_CAPS}, 467: {<<"hash">>, <<"sha-1">>}, 468: {<<"node">>, caps_node_name()}, 469: {<<"ver">>, caps_hash(PEPNodeNS)}]}. 470: 471: features(PEPNodeNS) -> 472: [?NS_DISCO_INFO, 473: ?NS_DISCO_ITEMS, 474: ?NS_GEOLOC, 475: ns_notify(?NS_GEOLOC), 476: PEPNodeNS, 477: ns_notify(PEPNodeNS)]. 478: 479: ns_notify(NS) -> 480: <<NS/binary, "+notify">>. 481: 482: random_node_ns() -> 483: base64:encode(crypto:strong_rand_bytes(16)). 484: 485: caps_hash(PEPNodeNS) -> 486: rpc(mim(), mod_caps, make_disco_hash, [feature_elems(PEPNodeNS), sha1]). 487: 488: caps_node_name() -> 489: <<"http://www.chatopus.com">>. 490: 491: send_presence(From, Type, To) -> 492: ToJid = escalus_client:short_jid(To), 493: Stanza = escalus_stanza:presence_direct(ToJid, Type), 494: escalus_client:send(From, Stanza). 495: 496: make_friends(Bob, Alice) -> 497: % makes uni-directional presence subscriptions 498: % returns stanzas received finally by the inviter 499: send_presence(Bob, <<"subscribe">>, Alice), 500: send_presence(Alice, <<"subscribed">>, Bob), 501: escalus:wait_for_stanzas(Alice, 10, 200), 502: BobStanzas = escalus:wait_for_stanzas(Bob, 10, 200), 503: lists:filter(fun(S) -> N = S#xmlel.name, 504: N =/= <<"iq">> 505: andalso 506: N =/= <<"presence">> 507: andalso 508: N =/= <<"r">> 509: end, 510: BobStanzas). 511: 512: publish_with_sm(User, ItemId, Node, Options) -> 513: Id = id(User, Node, <<"publish">>), 514: Request = case proplists:get_value(with_payload, Options, true) of 515: true -> escalus_pubsub_stanza:publish(User, ItemId, item_content(), Id, Node); 516: false -> escalus_pubsub_stanza:publish(User, Id, Node) 517: end, 518: escalus_client:send(User, Request), 519: escalus:wait_for_stanzas(User, 2). 520: 521: id(User, {NodeAddr, NodeName}, Suffix) -> 522: UserName = escalus_utils:get_username(User), 523: list_to_binary(io_lib:format("~s-~s-~s-~s", [UserName, NodeAddr, NodeName, Suffix])). 524: 525: item_content() -> 526: #xmlel{name = <<"entry">>, 527: attrs = [{<<"xmlns">>, <<"http://www.w3.org/2005/Atom">>}]}. 528: 529: enable_sm(User) -> 530: escalus_client:send(User, escalus_stanza:enable_sm()), 531: #xmlel{name = <<"enabled">>} = escalus:wait_for_stanza(User).