1: -module(smart_markers_SUITE). 2: -compile([export_all, nowarn_export_all]). 3: 4: -include_lib("common_test/include/ct.hrl"). 5: -include_lib("stdlib/include/assert.hrl"). 6: -include_lib("escalus/include/escalus_xmlns.hrl"). 7: -include_lib("exml/include/exml.hrl"). 8: -include("muc_light.hrl"). 9: -define(NS_ESL_SMART_MARKERS, <<"esl:xmpp:smart-markers:0">>). 10: -define(NS_STANZAID, <<"urn:xmpp:sid:0">>). 11: 12: -import(distributed_helper, [mim/0, rpc/4, subhost_pattern/1]). 13: -import(domain_helper, [host_type/0]). 14: -import(config_parser_helper, [mod_config/2]). 15: 16: %%% Suite configuration 17: all() -> 18: case (not ct_helper:is_ct_running()) 19: orelse mongoose_helper:is_rdbms_enabled(host_type()) of 20: true -> all_cases(); 21: false -> {skip, require_rdbms} 22: end. 23: 24: all_cases() -> 25: [ 26: {group, regular}, 27: {group, async_pools} 28: ]. 29: 30: groups() -> 31: inbox_helper:maybe_run_in_parallel(groups1()) ++ groups2(). 32: 33: groups1() -> 34: [ 35: {one2one, [], 36: [ 37: error_set_iq, 38: error_bad_peer, 39: error_no_peer_given, 40: error_bad_timestamp, 41: marker_is_stored, 42: marker_can_be_fetched, 43: marker_for_thread_can_be_fetched, 44: marker_after_timestamp_can_be_fetched, 45: marker_after_timestamp_for_threadid_can_be_fetched, 46: remove_markers_when_removed_user, 47: repeated_markers_produce_no_warnings 48: ]}, 49: {muclight, [], 50: [ 51: marker_is_stored_for_room, 52: marker_can_be_fetched_for_room, 53: marker_is_removed_when_user_leaves_room, 54: markers_are_removed_when_room_is_removed 55: ]}, 56: {keep_private, [], 57: [ 58: marker_is_not_routed_nor_fetchable, 59: fetching_room_answers_only_own_marker 60: ]} 61: ]. 62: 63: groups2() -> 64: [ 65: {regular, [], 66: [ 67: {group, one2one}, 68: {group, muclight}, 69: {group, keep_private} 70: ]}, 71: {async_pools, [], 72: [ 73: {group, one2one}, 74: {group, muclight}, 75: {group, keep_private} 76: ]} 77: ]. 78: 79: suite() -> 80: escalus:suite(). 81: 82: init_per_suite(Config) -> 83: escalus:init_per_suite(Config). 84: 85: end_per_suite(Config) -> 86: escalus:end_per_suite(Config). 87: 88: init_per_group(regular, Config) -> 89: [{merge_opts, #{backend => rdbms}} | Config]; 90: init_per_group(async_pools, Config) -> 91: [{merge_opts, #{backend => rdbms_async, 92: async_writer => #{pool_size => 2}}} | Config]; 93: init_per_group(GroupName, Config) -> 94: AsyncType = ?config(merge_opts, Config), 95: HostType = domain_helper:host_type(), 96: Config1 = dynamic_modules:save_modules(HostType, Config), 97: ok = dynamic_modules:ensure_modules(HostType, group_to_module(GroupName, AsyncType)), 98: Config1. 99: 100: group_to_module(one2one, MergeOpts) -> 101: [{mod_smart_markers, mod_config(mod_smart_markers, MergeOpts)}]; 102: group_to_module(keep_private, MergeOpts) -> 103: [{mod_smart_markers, mod_config(mod_smart_markers, MergeOpts#{keep_private => true})}, 104: {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}]; 105: group_to_module(muclight, MergeOpts) -> 106: [{mod_smart_markers, mod_config(mod_smart_markers, MergeOpts)}, 107: {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}]. 108: 109: end_per_group(muclight, Config) -> 110: muc_light_helper:clear_db(host_type()), 111: end_per_group(generic, Config); 112: end_per_group(_, Config) -> 113: escalus_fresh:clean(), 114: dynamic_modules:restore_modules(Config), 115: Config. 116: 117: init_per_testcase(repeated_markers_produce_no_warnings = TC, Config) -> 118: logger_ct_backend:start(), 119: escalus:init_per_testcase(TC, Config); 120: init_per_testcase(Name, Config) -> 121: escalus:init_per_testcase(Name, Config). 122: 123: end_per_testcase(repeated_markers_produce_no_warnings = TC, Config) -> 124: logger_ct_backend:stop(), 125: escalus:end_per_testcase(TC, Config); 126: end_per_testcase(Name, Config) -> 127: escalus:end_per_testcase(Name, Config). 128: 129: %%% tests 130: error_set_iq(Config) -> 131: escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> 132: Query = escalus_stanza:query_el(?NS_ESL_SMART_MARKERS, []), 133: Iq = escalus_stanza:iq(<<"set">>, [Query]), 134: escalus:send(Alice, Iq), 135: Response = escalus:wait_for_stanza(Alice), 136: escalus:assert(is_iq_error, [Iq], Response) 137: end). 138: 139: error_bad_peer(Config) -> 140: escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> 141: Iq = iq_fetch_marker([{<<"peer">>, <<"/@">>}]), 142: escalus:send(Alice, Iq), 143: Response = escalus:wait_for_stanza(Alice), 144: escalus:assert(is_iq_error, [Iq], Response) 145: end). 146: 147: error_no_peer_given(Config) -> 148: escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> 149: Iq = iq_fetch_marker([]), 150: escalus:send(Alice, Iq), 151: Response = escalus:wait_for_stanza(Alice), 152: escalus:assert(is_iq_error, [Iq], Response) 153: end). 154: 155: error_bad_timestamp(Config) -> 156: escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> 157: PeerJid = <<"peer@localhost">>, 158: Iq = iq_fetch_marker([{<<"peer">>, PeerJid}, {<<"after">>, <<"baddate">>}]), 159: escalus:send(Alice, Iq), 160: Response = escalus:wait_for_stanza(Alice), 161: escalus:assert(is_iq_error, [Iq], Response) 162: end). 163: 164: marker_is_stored(Config) -> 165: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 166: send_message_respond_marker(Alice, Bob), 167: AliceJid = jid:from_binary(escalus_client:full_jid(Alice)), 168: BobJid = jid:from_binary(escalus_client:full_jid(Bob)), 169: mongoose_helper:wait_until( 170: fun() -> length(fetch_markers_for_users(BobJid, AliceJid)) > 0 end, true) 171: end). 172: 173: marker_can_be_fetched(Config) -> 174: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 175: send_message_respond_marker(Alice, Bob), 176: send_message_respond_marker(Bob, Alice), 177: verify_marker_fetch(Bob, Alice), 178: verify_marker_fetch(Alice, Bob) 179: end). 180: 181: marker_is_not_routed_nor_fetchable(Config) -> 182: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 183: MsgId = escalus_stanza:id(), 184: Msg = escalus_stanza:set_id(escalus_stanza:chat_to(Bob, <<"Hello!">>), MsgId), 185: escalus:send(Alice, Msg), 186: escalus:wait_for_stanza(Bob), 187: ChatMarker = escalus_stanza:chat_marker(Alice, <<"displayed">>, MsgId), 188: escalus:send(Bob, ChatMarker), 189: escalus_assert:has_no_stanzas(Alice), %% Marker is filtered, Alice won't receive the marker 190: verify_marker_fetch_is_empty(Alice, Bob), %% Alice won't see Bob's marker 191: verify_marker_fetch(Bob, Alice) %% Bob will see his own marker 192: end). 193: 194: fetching_room_answers_only_own_marker(Config) -> 195: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) -> 196: Users = [Alice, Bob, Kate], 197: RoomId = create_room(Alice, [Bob, Kate], Config), 198: RoomBinJid = muc_light_helper:room_bin_jid(RoomId), 199: send_msg_to_room(Users, RoomBinJid, Alice, escalus_stanza:id()), 200: send_msg_to_room(Users, RoomBinJid, Bob, escalus_stanza:id()), 201: MsgId = send_msg_to_room(Users, RoomBinJid, Kate, escalus_stanza:id()), 202: ChatMarker = escalus_stanza:setattr( 203: escalus_stanza:chat_marker(RoomBinJid, <<"displayed">>, MsgId), 204: <<"type">>, <<"groupchat">>), 205: [ begin 206: escalus:send(User, ChatMarker), 207: {ok, MarkersThatUserHasInRoom} = verify_marker_fetch(User, RoomBinJid), 208: ?assertEqual(1, length(MarkersThatUserHasInRoom)) 209: end || User <- [Alice, Bob] ] 210: end). 211: 212: marker_for_thread_can_be_fetched(Config) -> 213: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 214: ThreadId = <<"some-thread-id">>, 215: send_message_respond_marker(Alice, Bob), 216: send_message_respond_marker(Alice, Bob, ThreadId), 217: verify_marker_fetch(Bob, Alice, ThreadId, undefined) 218: end). 219: 220: marker_after_timestamp_can_be_fetched(Config) -> 221: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 222: TS = rpc(mim(), erlang, system_time, [microsecond]), 223: BinTS = list_to_binary(calendar:system_time_to_rfc3339(TS, [{offset, "Z"}, {unit, microsecond}])), 224: send_message_respond_marker(Alice, Bob), 225: send_message_respond_marker(Alice, Bob), 226: verify_marker_fetch(Bob, Alice, undefined, BinTS) 227: end). 228: 229: marker_after_timestamp_for_threadid_can_be_fetched(Config) -> 230: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 231: ThreadId = <<"some-thread-id">>, 232: TS = rpc(mim(), erlang, system_time, [microsecond]), 233: BinTS = list_to_binary(calendar:system_time_to_rfc3339(TS, [{offset, "Z"}, {unit, microsecond}])), 234: send_message_respond_marker(Alice, Bob), 235: send_message_respond_marker(Alice, Bob, ThreadId), 236: verify_marker_fetch(Bob, Alice, ThreadId, BinTS) 237: end). 238: 239: remove_markers_when_removed_user(Config) -> 240: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 241: Body = <<"Hello Bob!">>, 242: MsgId = escalus_stanza:id(), 243: Msg = escalus_stanza:set_id(escalus_stanza:chat_to(Bob, Body), MsgId), 244: escalus:send(Alice, Msg), 245: escalus:wait_for_stanza(Bob), 246: ChatMarker = escalus_stanza:chat_marker(Alice, <<"displayed">>, MsgId), 247: escalus:send(Bob, ChatMarker), 248: escalus:wait_for_stanza(Alice), 249: AliceJid = jid:from_binary(escalus_client:full_jid(Alice)), 250: BobJid = jid:from_binary(escalus_client:full_jid(Bob)), 251: mongoose_helper:wait_until(fun() -> length(fetch_markers_for_users(BobJid, AliceJid)) > 0 end, true), 252: unregister_user(Bob), 253: mongoose_helper:wait_until(fun() -> length(fetch_markers_for_users(BobJid, AliceJid)) end, 0) 254: end). 255: 256: repeated_markers_produce_no_warnings(Config) -> 257: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 258: MsgId = escalus_stanza:id(), 259: Msg = escalus_stanza:set_id(escalus_stanza:chat_to(Bob, <<"Hello!">>), MsgId), 260: escalus:send(Alice, Msg), 261: escalus:wait_for_stanza(Bob), 262: ChatMarker = escalus_stanza:chat_marker(Alice, <<"displayed">>, MsgId), 263: [MarkerEl] = ChatMarker#xmlel.children, 264: RepeatedChatMarker = ChatMarker#xmlel{children = [MarkerEl, MarkerEl]}, 265: logger_ct_backend:capture(warning), 266: escalus:send(Bob, RepeatedChatMarker), 267: escalus:wait_for_stanza(Alice), 268: logger_ct_backend:stop_capture(), 269: FilterFun = fun(_, WMsg) -> re:run(WMsg, "{bad_result,{updated,0}}") /= nomatch end, 270: [] = logger_ct_backend:recv(FilterFun) 271: end). 272: 273: marker_is_stored_for_room(Config) -> 274: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], 275: fun(Alice, Bob, Kate) -> 276: Users = [Alice, Bob, Kate], 277: RoomId = create_room(Alice, [Bob, Kate], Config), 278: RoomBinJid = muc_light_helper:room_bin_jid(RoomId), 279: one_marker_in_room(Users, RoomBinJid, Alice, Bob), 280: BobJid = jid:from_binary(escalus_client:full_jid(Bob)), 281: mongoose_helper:wait_until( 282: fun() -> length(fetch_markers_for_users(BobJid, jid:from_binary(RoomBinJid))) > 0 end, true) 283: end). 284: 285: marker_can_be_fetched_for_room(Config) -> 286: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], 287: fun(Alice, Bob, Kate) -> 288: Users = [Alice, Bob, Kate], 289: RoomId = create_room(Alice, [Bob, Kate], Config), 290: RoomBinJid = muc_light_helper:room_bin_jid(RoomId), 291: one_marker_in_room(Users, RoomBinJid, Alice, Bob), 292: verify_marker_fetch(Bob, RoomBinJid) 293: end). 294: 295: marker_is_removed_when_user_leaves_room(Config) -> 296: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], 297: fun(Alice, Bob) -> 298: Users = [Alice, Bob], 299: RoomId = create_room(Alice, [Bob], Config), 300: RoomBinJid = muc_light_helper:room_bin_jid(RoomId), 301: RoomJid = jid:from_binary(RoomBinJid), 302: one_marker_in_room(Users, RoomBinJid, Alice, Bob), 303: BobJid = jid:from_binary(escalus_client:full_jid(Bob)), 304: mongoose_helper:wait_until( 305: fun() -> length(fetch_markers_for_users(BobJid, RoomJid)) > 0 end, true), 306: % Remove Bob from the room 307: muc_light_helper:user_leave(RoomId, Bob, [Alice]), 308: mongoose_helper:wait_until( 309: fun() -> length(fetch_markers_for_users(BobJid, RoomJid)) end, 0) 310: end). 311: 312: markers_are_removed_when_room_is_removed(Config) -> 313: escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> 314: Users = [Alice, Bob], 315: RoomId = create_room(Alice, [Bob], Config), 316: RoomBinJid = muc_light_helper:room_bin_jid(RoomId), 317: RoomJid = jid:from_binary(RoomBinJid), 318: one_marker_in_room(Users, RoomBinJid, Alice, Bob), 319: BobJid = jid:from_binary(escalus_client:full_jid(Bob)), 320: mongoose_helper:wait_until( 321: fun() -> length(fetch_markers_for_users(BobJid, RoomJid)) > 0 end, true), 322: %% The room is then deleted 323: delete_room(Alice, Users, RoomBinJid), 324: [ begin 325: Jid = jid:from_binary(escalus_client:full_jid(User)), 326: mongoose_helper:wait_until( 327: fun() -> length(fetch_markers_for_users(Jid, RoomJid)) end, 0) 328: end || User <- Users ] 329: end). 330: 331: %%% helpers 332: fetch_markers_for_users(From, To) -> 333: MRs = rpc(mim(), mod_smart_markers_backend, get_chat_markers, 334: [host_type(), To, undefined, 0]), 335: [MR || #{from := FR} = MR <- MRs, jid:are_bare_equal(From, FR)]. 336: 337: iq_fetch_marker(Attrs) -> 338: Query = escalus_stanza:query_el(?NS_ESL_SMART_MARKERS, Attrs, []), 339: escalus_stanza:iq(<<"get">>, [Query]). 340: 341: create_room(Owner, Members, Config) -> 342: RoomId = muc_helper:fresh_room_name(), 343: MucHost = muc_light_helper:muc_host(), 344: muc_light_helper:create_room(RoomId, MucHost, Owner, Members, Config, muc_light_helper:ver(1)), 345: RoomId. 346: 347: delete_room(Owner, Users, RoomBinJid) -> 348: Destroy = escalus_stanza:to(escalus_stanza:iq_set(?NS_MUC_LIGHT_DESTROY, []), RoomBinJid), 349: escalus:send(Owner, Destroy), 350: AffUsersChanges = [{User, none} || User <- Users ], 351: muc_light_helper:verify_aff_bcast([], AffUsersChanges, [?NS_MUC_LIGHT_DESTROY]), 352: escalus:assert(is_iq_result, escalus:wait_for_stanza(Owner)). 353: 354: one_marker_in_room(Users, RoomBinJid, Writer, Marker) -> 355: MsgId = escalus_stanza:id(), 356: StanzaId = send_msg_to_room(Users, RoomBinJid, Writer, MsgId), 357: mark_msg(Users, RoomBinJid, Marker, StanzaId). 358: 359: send_msg_to_room(Users, RoomBinJid, Writer, MsgId) -> 360: Msg = escalus_stanza:set_id(escalus_stanza:groupchat_to(RoomBinJid, <<"Hello">>), MsgId), 361: escalus:send(Writer, Msg), 362: Msgs = [ escalus:wait_for_stanza(User) || User <- Users ], 363: get_id(hd(Msgs), MsgId). 364: 365: mark_msg(Users, RoomBinJid, Marker, StanzaId) -> 366: ChatMarker = escalus_stanza:setattr( 367: escalus_stanza:chat_marker(RoomBinJid, <<"displayed">>, StanzaId), 368: <<"type">>, <<"groupchat">>), 369: escalus:send(Marker, ChatMarker), 370: [ escalus:wait_for_stanza(User) || User <- Users ], 371: StanzaId. 372: 373: send_message_respond_marker(MsgWriter, MarkerAnswerer) -> 374: send_message_respond_marker(MsgWriter, MarkerAnswerer, undefined). 375: 376: send_message_respond_marker(MsgWriter, MarkerAnswerer, MaybeThread) -> 377: MsgId = escalus_stanza:id(), 378: Msg = add_thread_id(escalus_stanza:set_id( 379: escalus_stanza:chat_to(MarkerAnswerer, <<"Hello!">>), 380: MsgId), 381: MaybeThread), 382: escalus:send(MsgWriter, Msg), 383: escalus:wait_for_stanza(MarkerAnswerer), 384: ChatMarker = add_thread_id(escalus_stanza:chat_marker( 385: MsgWriter, <<"displayed">>, MsgId), 386: MaybeThread), 387: escalus:send(MarkerAnswerer, ChatMarker), 388: escalus:wait_for_stanza(MsgWriter). 389: 390: verify_marker_fetch(MarkingUser, MarkedUser) -> 391: verify_marker_fetch(MarkingUser, MarkedUser, undefined, undefined). 392: 393: verify_marker_fetch(MarkingUser, MarkedUser, Thread, After) -> 394: MarkedUserBJid = case is_binary(MarkedUser) of 395: true -> [{<<"peer">>, MarkedUser}]; 396: false -> [{<<"peer">>, escalus_utils:jid_to_lower(escalus_client:short_jid(MarkedUser))}] 397: end, 398: MaybeThread = case Thread of 399: undefined -> []; 400: _ -> [{<<"thread">>, Thread}] 401: end, 402: MaybeAfter = case After of 403: undefined -> []; 404: _ -> [{<<"after">>, After}] 405: end, 406: Iq = iq_fetch_marker(MarkedUserBJid ++ MaybeThread ++ MaybeAfter), 407: %% using wait_until to ensure that assertions are eventually passing 408: mongoose_helper:wait_until( 409: fun() -> 410: escalus:send(MarkingUser, Iq), 411: Response = escalus:wait_for_stanza(MarkingUser), 412: escalus:assert(is_iq_result, [Iq], Response), 413: Markers = [Marker | _] = exml_query:paths( 414: Response, [{element_with_ns, <<"query">>, ?NS_ESL_SMART_MARKERS}, 415: {element, <<"marker">>}]), 416: ?assertNotEqual(undefined, Marker), 417: ?assertNotEqual(undefined, exml_query:attr(Marker, <<"timestamp">>)), 418: ?assertEqual(<<"displayed">>, exml_query:attr(Marker, <<"type">>)), 419: ?assertEqual(Thread, exml_query:attr(Marker, <<"thread">>)), 420: ?assertNotEqual(undefined, exml_query:attr(Marker, <<"id">>)), 421: lists:sort(Markers) 422: end, 423: fun(_) -> true end, %% always positively validate the return value. 424: #{name => fetch_marker}). 425: 426: verify_marker_fetch_is_empty(MarkingUser, MarkedUser) -> 427: MarkedUserBJid = escalus_utils:jid_to_lower(escalus_client:short_jid(MarkedUser)), 428: Iq = iq_fetch_marker([{<<"peer">>, MarkedUserBJid}]), 429: escalus:send(MarkingUser, Iq), 430: Response = escalus:wait_for_stanza(MarkingUser), 431: escalus:assert(is_iq_result, [Iq], Response), 432: Markers = exml_query:paths(Response, [{element_with_ns, <<"query">>, ?NS_ESL_SMART_MARKERS}, 433: {element, <<"marker">>}]), 434: ?assertEqual([], Markers). 435: 436: get_id(Packet, Def) -> 437: exml_query:path( 438: Packet, [{element_with_ns, <<"stanza-id">>, ?NS_STANZAID}, {attr, <<"id">>}], Def). 439: 440: add_thread_id(Msg, undefined) -> 441: Msg; 442: add_thread_id(#xmlel{children = Children} = Msg, ThreadID) -> 443: ThreadEl = #xmlel{name = <<"thread">>, 444: children = [#xmlcdata{content = ThreadID}]}, 445: Msg#xmlel{children = [ThreadEl | Children]}. 446: 447: unregister_user(Client) -> 448: Jid = jid:from_binary(escalus_client:short_jid(Client)), 449: rpc(mim(), ejabberd_auth, remove_user, [Jid]).