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