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