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