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