1: %%%=================================================================== 2: %%% @copyright (C) 2011, Erlang Solutions Ltd. 3: %%% @doc Suite for testing mod_offline* modules 4: %%% @end 5: %%%=================================================================== 6: 7: -module(offline_SUITE). 8: -compile([export_all, nowarn_export_all]). 9: 10: -include_lib("escalus/include/escalus.hrl"). 11: -include_lib("common_test/include/ct.hrl"). 12: -include_lib("exml/include/exml.hrl"). 13: 14: -define(MAX_OFFLINE_MSGS, 100). % known server-side config 15: 16: -define(DELAY_NS, <<"urn:xmpp:delay">>). 17: -define(AFFILIATION_NS, <<"urn:xmpp:muclight:0#affiliations">>). 18: -define(NS_FEATURE_MSGOFFLINE, <<"msgoffline">>). 19: 20: -import(domain_helper, [host_type/0]). 21: -import(mongoose_helper, [wait_for_n_offline_messages/2]). 22: -import(config_parser_helper, [mod_config/2, mod_config_with_auto_backend/1]). 23: 24: %%%=================================================================== 25: %%% Suite configuration 26: %%%=================================================================== 27: 28: all() -> 29: [{group, mod_offline_tests}, 30: {group, chatmarkers}, 31: {group, with_groupchat}]. 32: 33: all_tests() -> 34: [disco_info_sm, 35: offline_message_is_stored_and_delivered_at_login, 36: error_message_is_not_stored, 37: groupchat_message_is_not_stored, 38: headline_message_is_not_stored, 39: expired_messages_are_not_delivered, 40: max_offline_messages_reached]. 41: 42: chat_markers_tests() -> 43: [one2one_chatmarker_is_overriden_and_only_unique_markers_are_delivered, 44: room_chatmarker_is_overriden_and_only_unique_markers_are_delivered]. 45: 46: groups() -> 47: G = [{mod_offline_tests, [parallel], all_tests()}, 48: {with_groupchat, [], [groupchat_message_is_stored]}, 49: {chatmarkers, [], chat_markers_tests()} 50: ], 51: ct_helper:repeat_all_until_all_ok(G). 52: 53: suite() -> 54: escalus:suite(). 55: 56: %%%=================================================================== 57: %%% Init & teardown 58: %%%=================================================================== 59: 60: init_per_suite(Config0) -> 61: HostType = domain_helper:host_type(), 62: Config1 = dynamic_modules:save_modules(HostType, Config0), 63: dynamic_modules:ensure_modules(HostType, create_config()), 64: escalus:init_per_suite(Config1). 65: 66: -spec create_config() -> [{mod_offline, gen_mod:module_opts()}]. 67: create_config() -> 68: [{mod_offline, mod_config_with_auto_backend(mod_offline)}]. 69: 70: end_per_suite(Config) -> 71: escalus_fresh:clean(), 72: dynamic_modules:restore_modules(Config), 73: escalus:end_per_suite(Config). 74: 75: init_per_group(with_groupchat, C) -> 76: Config = dynamic_modules:save_modules(host_type(), C), 77: dynamic_modules:ensure_modules(host_type(), with_groupchat_modules()), 78: Config; 79: init_per_group(chatmarkers, C) -> 80: case mongoose_helper:is_rdbms_enabled(host_type()) of 81: false -> 82: {skip, require_rdbms}; 83: true -> 84: Config = dynamic_modules:save_modules(host_type(), C), 85: dynamic_modules:ensure_modules(host_type(), chatmarkers_modules()), 86: Config 87: end; 88: init_per_group(_, C) -> C. 89: 90: with_groupchat_modules() -> 91: OfflineBackend = mongoose_helper:get_backend_name(host_type(), mod_offline), 92: MucLightBackend = mongoose_helper:mnesia_or_rdbms_backend(), 93: [{mod_offline, config_with_groupchat_modules(OfflineBackend)}, 94: {mod_muc_light, mod_config(mod_muc_light, #{backend => MucLightBackend})}]. 95: 96: config_with_groupchat_modules(Backend) -> 97: mod_config(mod_offline, #{store_groupchat_messages => true, 98: backend => Backend}). 99: 100: chatmarkers_modules() -> 101: [{mod_smart_markers, config_parser_helper:default_mod_config(mod_smart_markers)}, 102: {mod_offline, config_with_groupchat_modules(rdbms)}, 103: {mod_offline_chatmarkers, 104: mod_config(mod_offline_chatmarkers, 105: #{store_groupchat_messages => true})}, 106: {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}]. 107: 108: end_per_group(Group, C) when Group =:= chatmarkers; 109: Group =:= with_groupchat -> 110: dynamic_modules:restore_modules(C), 111: C; 112: end_per_group(_, C) -> C. 113: 114: init_per_testcase(Name, C) -> escalus:init_per_testcase(Name, C). 115: end_per_testcase(Name, C) -> escalus:end_per_testcase(Name, C). 116: 117: %%%=================================================================== 118: %%% offline tests 119: %%%=================================================================== 120: 121: disco_info_sm(Config) -> 122: escalus:fresh_story(Config, [{alice, 1}], 123: fun(Alice) -> 124: AliceJid = escalus_client:short_jid(Alice), 125: escalus:send(Alice, escalus_stanza:disco_info(AliceJid)), 126: Stanza = escalus:wait_for_stanza(Alice), 127: escalus:assert(has_feature, [?NS_FEATURE_MSGOFFLINE], Stanza), 128: escalus:assert(is_stanza_from, [AliceJid], Stanza) 129: end). 130: 131: offline_message_is_stored_and_delivered_at_login(Config) -> 132: Story = 133: fun(FreshConfig, Alice, Bob) -> 134: logout(FreshConfig, Bob), 135: escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"msgtxt">>)), 136: wait_for_n_offline_messages(Bob, 1), 137: NewBob = login_send_presence(FreshConfig, bob), 138: Stanzas = escalus:wait_for_stanzas(NewBob, 2), 139: escalus_new_assert:mix_match 140: ([is_presence, is_chat(<<"msgtxt">>)], 141: Stanzas) 142: end, 143: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], Story). 144: 145: error_message_is_not_stored(Config) -> 146: Story = fun(FreshConfig, Alice, Bob) -> 147: logout(FreshConfig, Bob), 148: AliceJid = escalus_client:full_jid(Alice), 149: escalus:send(Alice, escalus_stanza:message 150: (AliceJid, Bob, <<"error">>, <<"msgtxt">>)), 151: NewBob = login_send_and_receive_presence(FreshConfig, bob), 152: ct:sleep(500), 153: false = escalus_client:has_stanzas(NewBob) 154: end, 155: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], Story). 156: 157: groupchat_message_is_not_stored(Config) -> 158: Story = fun(FreshConfig, Alice, Bob) -> 159: logout(FreshConfig, Bob), 160: AliceJid = escalus_client:full_jid(Alice), 161: escalus:send(Alice, escalus_stanza:message 162: (AliceJid, Bob, <<"groupchat">>, <<"msgtxt">>)), 163: NewBob = login_send_and_receive_presence(FreshConfig, bob), 164: ct:sleep(500), 165: false = escalus_client:has_stanzas(NewBob) 166: end, 167: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], Story). 168: 169: groupchat_message_is_stored(Config) -> 170: Story = fun(FreshConfig, Alice, Bob) -> 171: CreateRoomStanza = muc_light_helper:stanza_create_room(undefined, [], 172: [{Bob, member}]), 173: logout(FreshConfig, Bob), 174: escalus:send(Alice, CreateRoomStanza), 175: AffMsg = escalus:wait_for_stanza(Alice), 176: RoomJID = exml_query:attr(AffMsg, <<"from">>), 177: escalus:send(Alice, escalus_stanza:groupchat_to(RoomJID, <<"msgtxt">>)), 178: wait_for_n_offline_messages(Bob, 2), 179: NewBob = login_send_presence(FreshConfig, bob), 180: Stanzas = escalus:wait_for_stanzas(NewBob, 3), 181: escalus_new_assert:mix_match([is_presence, is_affiliation(), 182: is_groupchat(<<"msgtxt">>)], 183: Stanzas) 184: end, 185: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], Story). 186: 187: one2one_chatmarker_is_overriden_and_only_unique_markers_are_delivered(Config) -> 188: Story = 189: fun(FreshConfig, Alice, Bob) -> 190: logout(FreshConfig, Bob), 191: escalus:send(Alice, one2one_chatmarker(Bob, <<"received">>, <<"123">>)), 192: escalus:send(Alice, one2one_chatmarker(Bob, <<"received">>, <<"321">>)), 193: escalus:send(Alice, one2one_chatmarker(Bob, <<"received">>, <<"322">>, <<"t1">>)), 194: escalus:send(Alice, one2one_chatmarker(Bob, <<"received">>, <<"323">>, <<"t1">>)), 195: escalus:send(Alice, one2one_chatmarker(Bob, <<"displayed">>, <<"319">>)), 196: escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"msgtxt">>)), 197: wait_for_n_offline_messages(Bob, 1), 198: NewBob = login_send_presence(FreshConfig, bob), 199: Stanzas = escalus:wait_for_stanzas(NewBob, 6), %only 5 messages must be received 200: escalus_new_assert:mix_match( 201: [is_presence, is_chat(<<"msgtxt">>), 202: is_one2one_chatmarker(<<"received">>, <<"321">>), 203: is_one2one_chatmarker(<<"received">>, <<"323">>, <<"t1">>), 204: is_one2one_chatmarker(<<"displayed">>, <<"319">>)], 205: Stanzas) 206: end, 207: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], Story). 208: 209: room_chatmarker_is_overriden_and_only_unique_markers_are_delivered(Config) -> 210: Story = 211: fun(FreshConfig, Alice, Bob) -> 212: CreateRoomStanza = muc_light_helper:stanza_create_room(undefined, [], 213: [{Bob, member}]), 214: escalus:send(Alice, CreateRoomStanza), 215: AffMsg = escalus:wait_for_stanza(Alice), 216: RoomJID = exml_query:attr(AffMsg, <<"from">>), 217: AffMsg2 = escalus:wait_for_stanza(Bob), 218: RoomJID = exml_query:attr(AffMsg2, <<"from">>), 219: logout(FreshConfig, Bob), 220: escalus:send(Alice, room_chatmarker(RoomJID, <<"received">>, <<"123">>)), 221: escalus:send(Alice, room_chatmarker(RoomJID, <<"received">>, <<"321">>)), 222: escalus:send(Alice, room_chatmarker(RoomJID, <<"received">>, <<"322">>, <<"t1">>)), 223: escalus:send(Alice, room_chatmarker(RoomJID, <<"received">>, <<"323">>, <<"t1">>)), 224: escalus:send(Alice, room_chatmarker(RoomJID, <<"displayed">>, <<"319">>)), 225: escalus:send(Alice, escalus_stanza:groupchat_to(RoomJID, <<"msgtxt">>)), 226: wait_for_n_offline_messages(Bob, 1), 227: NewBob = login_send_presence(FreshConfig, bob), 228: Stanzas = escalus:wait_for_stanzas(NewBob, 6), %only 5 messages must be received 229: escalus_new_assert:mix_match( 230: [is_presence, is_groupchat(<<"msgtxt">>), 231: is_room_chatmarker(<<"received">>, <<"321">>), 232: is_room_chatmarker(<<"received">>, <<"323">>, <<"t1">>), 233: is_room_chatmarker(<<"displayed">>, <<"319">>)], 234: Stanzas) 235: end, 236: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], Story). 237: 238: headline_message_is_not_stored(Config) -> 239: Story = fun(FreshConfig, Alice, Bob) -> 240: logout(FreshConfig, Bob), 241: AliceJid = escalus_client:full_jid(Alice), 242: escalus:send(Alice, escalus_stanza:message 243: (AliceJid, Bob, <<"headline">>, <<"msgtxt">>)), 244: NewBob = login_send_and_receive_presence(FreshConfig, bob), 245: ct:sleep(500), 246: false = escalus_client:has_stanzas(NewBob) 247: end, 248: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], Story). 249: 250: max_offline_messages_reached(Config) -> 251: Story = 252: fun(FreshConfig, Alice, B1, B2, B3, B4) -> 253: BobsResources = [B1,B2,B3,B4], 254: MessagesPerResource = ?MAX_OFFLINE_MSGS div length(BobsResources), 255: 256: logout(FreshConfig, Alice), 257: each_client_sends_messages_to(BobsResources, Alice, 258: {count, MessagesPerResource}), 259: wait_for_n_offline_messages(Alice, MessagesPerResource * 4), 260: 261: send_message(B1, Alice, ?MAX_OFFLINE_MSGS+1), 262: 263: Packet = escalus:wait_for_stanza(B1), 264: escalus:assert(is_error, [<<"wait">>, <<"resource-constraint">>], Packet), 265: 266: NewAlice = login_send_presence(FreshConfig, alice), 267: Preds = [is_chat(make_chat_text(I)) 268: || I <- repeat(lists:seq(1, MessagesPerResource), 269: length(BobsResources))], 270: escalus_new_assert:mix_match 271: ([is_presence | Preds], 272: escalus:wait_for_stanzas(NewAlice, ?MAX_OFFLINE_MSGS+1)), 273: ct:sleep(500), 274: false = escalus_client:has_stanzas(Alice) 275: end, 276: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 4}], Story). 277: 278: expired_messages_are_not_delivered(Config) -> 279: Story = 280: fun(FreshConfig, Alice, Bob) -> 281: BobJid = escalus_client:short_jid(Bob), 282: logout(FreshConfig, Bob), 283: escalus:send(Alice, 284: make_message_with_expiry(BobJid, 600000, <<"long">>)), 285: escalus:send(Alice, 286: make_message_with_expiry(BobJid, 1, <<"short">>)), 287: 288: ct:sleep(timer:seconds(2)), 289: NewBob = login_send_presence(FreshConfig, bob), 290: 291: escalus_new_assert:mix_match 292: ([is_presence, is_chat(<<"long">>)], 293: escalus:wait_for_stanzas(NewBob, 2, 5000)), 294: ct:sleep(500), 295: false = escalus_client:has_stanzas(NewBob) 296: end, 297: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], Story). 298: 299: 300: %%%=================================================================== 301: %%% Custom predicates 302: %%%=================================================================== 303: 304: room_chatmarker(RoomJID, MarkerType, MessageId) -> 305: ChatMarker = escalus_stanza:chat_marker(RoomJID, MarkerType, MessageId), 306: escalus_stanza:setattr(ChatMarker, <<"type">>, <<"groupchat">>). 307: 308: room_chatmarker(RoomJID, MarkerType, MessageId, ThreadID) -> 309: ChatMarker = room_chatmarker(RoomJID, MarkerType, MessageId), 310: add_thread_id(ChatMarker, ThreadID). 311: 312: one2one_chatmarker(RecipientJID, MarkerType, MessageId) -> 313: escalus_stanza:chat_marker(RecipientJID, MarkerType, MessageId). 314: 315: one2one_chatmarker(RecipientJID, MarkerType, MessageId, ThreadID) -> 316: ChatMarker = one2one_chatmarker(RecipientJID, MarkerType, MessageId), 317: add_thread_id(ChatMarker, ThreadID). 318: 319: add_thread_id(#xmlel{children = Children} = ChatMarker, ThreadID) -> 320: ThreadEl = #xmlel{name = <<"thread">>, 321: children = [#xmlcdata{content = ThreadID}]}, 322: ChatMarker#xmlel{children = [ThreadEl | Children]}. 323: 324: is_chat(Content) -> 325: fun(Stanza) -> 326: escalus_pred:is_chat_message(Content, Stanza) andalso 327: has_element_with_ns(Stanza, <<"delay">>, ?DELAY_NS) 328: end. 329: 330: is_groupchat(Content) -> 331: fun(Stanza) -> 332: escalus_pred:is_groupchat_message(Content, Stanza) andalso 333: has_element_with_ns(Stanza, <<"delay">>, ?DELAY_NS) 334: end. 335: 336: is_affiliation() -> 337: fun(Stanza) -> 338: has_element_with_ns(Stanza, <<"x">>, ?AFFILIATION_NS) andalso 339: has_element_with_ns(Stanza, <<"delay">>, ?DELAY_NS) 340: end. 341: 342: is_one2one_chatmarker(Marker, Id) -> 343: is_one2one_chatmarker(Marker, Id, undefined). 344: 345: is_one2one_chatmarker(Marker, Id, ThreadID) -> 346: fun(Stanza) -> 347: is_chatmarker(Stanza, Marker, undefined, Id, ThreadID) 348: end. 349: 350: is_room_chatmarker(Marker, Id) -> 351: is_room_chatmarker(Marker, Id, undefined). 352: 353: is_room_chatmarker(Marker, Id, ThreadID) -> 354: fun(Stanza) -> 355: is_chatmarker(Stanza, Marker, <<"groupchat">>, Id, ThreadID) 356: end. 357: 358: is_chatmarker(Stanza, Marker, Type, Id, ThreadID) -> 359: try 360: escalus_pred:is_chat_marker(Marker, Id, Stanza) andalso 361: escalus_pred:has_type(Type, Stanza) andalso 362: has_thread_id(Stanza, ThreadID) 363: catch 364: _:_ -> false 365: 366: end. 367: 368: has_thread_id(Stanza, undefined) -> 369: undefined =:= exml_query:subelement(Stanza, <<"thread">>); 370: has_thread_id(Stanza, ThreadID) -> 371: ThreadID == exml_query:path(Stanza, [{element, <<"thread">>}, cdata]). 372: 373: has_element_with_ns(Stanza, Element, NS) -> 374: [] =/= exml_query:subelements_with_name_and_ns(Stanza, Element, NS). 375: 376: %%%=================================================================== 377: %%% Helpers 378: %%%=================================================================== 379: logout(Config, User) -> 380: mongoose_helper:logout_user(Config, User). 381: 382: login_send_presence(Config, User) -> 383: {ok, Client} = escalus_client:start(Config, User, <<"new-session">>), 384: escalus:send(Client, escalus_stanza:presence(<<"available">>)), 385: Client. 386: 387: login_send_and_receive_presence(Config, User) -> 388: Client = login_send_presence(Config, User), 389: P = escalus_client:wait_for_stanza(Client), 390: escalus:assert(is_presence, P), 391: Client. 392: 393: each_client_sends_messages_to(Sources, Target, {count, N}) when is_list(Sources) -> 394: par:map 395: (fun(Source) -> 396: [ send_message(Source, Target, I) || I <- lists:seq(1,N) ] 397: end, 398: Sources). 399: 400: send_message(From, To, I) -> 401: escalus:send(From, escalus_stanza:chat_to(To, make_chat_text(I))), 402: timer:sleep(100). 403: 404: make_chat_text(I) -> 405: Number = integer_to_binary(I), 406: <<"Hi, Offline ", Number/binary>>. 407: 408: make_message_with_expiry(Target, Expiry, Text) -> 409: ExpiryBin = list_to_binary(integer_to_list(Expiry)), 410: Stanza = escalus_stanza:chat_to(Target, Text), 411: #xmlel{children = Children} = Stanza, 412: ExpiryElem = #xmlel{name = <<"x">>, 413: attrs = [{<<"xmlns">>, <<"jabber:x:expire">>}, 414: {<<"seconds">>, ExpiryBin}]}, 415: Stanza#xmlel{children = [ExpiryElem | Children]}. 416: 417: repeat(_L, 0) -> []; 418: repeat(L, N) -> L ++ repeat(L, N-1).