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