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