1: -module(carboncopy_SUITE).
    2: 
    3: -compile([export_all, nowarn_export_all]).
    4: -include_lib("common_test/include/ct.hrl").
    5: -include_lib("proper/include/proper.hrl").
    6: -include_lib("eunit/include/eunit.hrl").
    7: 
    8: -define(AE(Expected, Actual), ?assertEqual(Expected, Actual)).
    9: -define(BODY, <<"And pious action">>).
   10: 
   11: -import(mongoose_helper, [enable_carbons/1, disable_carbons/1]).
   12: -import(domain_helper, [domain/0]).
   13: 
   14: all() ->
   15:     [{group, one2one},
   16:      {group, muc}].
   17: 
   18: groups() ->
   19:     [{one2one, [parallel],
   20:         [discovering_support,
   21:          enabling_carbons,
   22:          disabling_carbons,
   23:          avoiding_carbons,
   24:          non_enabled_clients_dont_get_sent_carbons,
   25:          non_enabled_clients_dont_get_received_carbons,
   26:          enabled_single_resource_doesnt_get_carbons,
   27:          unavailable_resources_dont_get_carbons,
   28:          dropped_client_doesnt_create_duplicate_carbons,
   29:          prop_forward_received_chat_messages,
   30:          prop_forward_sent_chat_messages,
   31:          prop_normal_routing_to_bare_jid,
   32:          chat_message_is_carbon_copied,
   33:          normal_message_with_body_is_carbon_copied,
   34:          normal_message_with_receipt_is_carbon_copied,
   35:          normal_message_with_csn_is_carbon_copied,
   36:          normal_message_with_chat_marker_is_carbon_copied]},
   37:      {muc, [parallel],
   38:         [group_chat_is_not_carbon_copied,
   39:          local_user_to_muc_participant_is_carbon_copied,
   40:          muc_participant_to_local_user_is_not_carbon_copied]}].
   41: 
   42: %%%===================================================================
   43: %%% Overall setup/teardown
   44: %%%===================================================================
   45: init_per_suite(C) ->
   46:     escalus:init_per_suite(C).
   47: 
   48: end_per_suite(C) ->
   49:     escalus_fresh:clean(),
   50:     escalus:end_per_suite(C).
   51: 
   52: init_per_group(muc, Config) ->
   53:     muc_helper:load_muc(),
   54:     Config;
   55: init_per_group(_GroupName, Config) ->
   56:     Config.
   57: 
   58: end_per_group(muc, Config) ->
   59:     muc_helper:unload_muc(),
   60:     Config;
   61: end_per_group(_GroupName, Config) ->
   62:     Config.
   63: 
   64: %%%===================================================================
   65: %%% Testcase specific setup/teardown
   66: %%%===================================================================
   67: init_per_testcase(Name, C) ->
   68:     escalus:init_per_testcase(Name, C).
   69: 
   70: end_per_testcase(Name, C) ->
   71:     escalus:end_per_testcase(Name, C).
   72: 
   73: %%%===================================================================
   74: %%% Individual Test Cases (from groups() definition)
   75: %%%===================================================================
   76: discovering_support(Config) ->
   77:     escalus:fresh_story(
   78:       Config, [{alice, 1}],
   79:       fun(Alice) ->
   80:               IqGet = escalus_stanza:disco_info(domain()),
   81:               escalus_client:send(Alice, IqGet),
   82:               Result = escalus_client:wait_for_stanza(Alice),
   83:               escalus:assert(is_iq_result, [IqGet], Result),
   84:               escalus:assert(has_feature, [<<"urn:xmpp:carbons:2">>], Result)
   85:       end).
   86: 
   87: enabling_carbons(Config) ->
   88:     escalus:fresh_story(Config, [{alice, 1}], fun mongoose_helper:enable_carbons/1).
   89: 
   90: disabling_carbons(Config) ->
   91:     escalus:fresh_story(Config, [{alice, 1}],
   92:                         fun(Alice) -> enable_carbons(Alice),
   93:                                       disable_carbons(Alice) end).
   94: 
   95: avoiding_carbons(Config) ->
   96:     escalus:fresh_story(
   97:       Config, [{alice, 2}, {bob, 1}],
   98:       fun(Alice1, Alice2, Bob) ->
   99:               enable_carbons([Alice1, Alice2]),
  100:               Msg = escalus_stanza:chat_without_carbon_to(Bob, ?BODY),
  101:               escalus_client:send(Alice1, Msg),
  102:               BobReceived = escalus_client:wait_for_stanza(Bob),
  103:               escalus:assert(is_chat_message, [?BODY], BobReceived),
  104:               ?assertEqual([], escalus_client:wait_for_stanzas(Alice2, 1, 500)),
  105:               ?assertEqual([], escalus_client:peek_stanzas(Alice2))
  106:       end).
  107: 
  108: non_enabled_clients_dont_get_sent_carbons(Config) ->
  109:     escalus:fresh_story(
  110:       Config, [{alice, 2}, {bob, 1}],
  111:       fun(Alice1, Alice2, Bob) ->
  112:               Msg = escalus_stanza:chat_to(Bob, ?BODY),
  113:               escalus_client:send(Alice1, Msg),
  114:               BobReceived = escalus_client:wait_for_stanza(Bob),
  115:               escalus:assert(is_chat_message, [?BODY], BobReceived),
  116:               ?assertEqual([], escalus_client:wait_for_stanzas(Alice2, 1, 500)),
  117:               ?assertEqual([], escalus_client:peek_stanzas(Alice2))
  118:       end).
  119: 
  120: non_enabled_clients_dont_get_received_carbons(Config) ->
  121:     escalus:fresh_story(
  122:       Config, [{alice, 2}, {bob, 1}],
  123:       fun(Alice1, Alice2, Bob) ->
  124:               Msg = escalus_stanza:chat_to(Alice1, ?BODY),
  125:               escalus_client:send(Bob, Msg),
  126:               AliceReceived = escalus_client:wait_for_stanza(Alice1),
  127:               escalus:assert(is_chat_message, [?BODY], AliceReceived),
  128:               ?assertEqual([], escalus_client:wait_for_stanzas(Alice2, 1, 500)),
  129:               ?assertEqual([], escalus_client:peek_stanzas(Alice2))
  130:       end).
  131: 
  132: enabled_single_resource_doesnt_get_carbons(Config) ->
  133:     BobsMessages = [
  134:                     <<"There's such a thing as dwelling">>,
  135:                     <<"On the thought ourselves have nursed,">>,
  136:                     <<"And with scorn and courage telling">>,
  137:                     <<"The world to do its worst.">>
  138:                    ],
  139:     escalus:fresh_story(
  140:       Config, [{alice, 1}, {bob, 1}],
  141:       fun(Alice, Bob) ->
  142:               enable_carbons(Alice),
  143:               [ escalus_client:send(Bob, escalus_stanza:chat_to(Alice, M))
  144:                 || M <- BobsMessages ],
  145:               [ escalus:assert(is_chat_message, [M], escalus_client:wait_for_stanza(Alice))
  146:                 || M <- BobsMessages ]
  147:       end).
  148: 
  149: unavailable_resources_dont_get_carbons(Config) ->
  150:     escalus:fresh_story(
  151:       Config, [{alice, 2}, {bob, 1}],
  152:       fun(Alice1, Alice2, Bob) ->
  153:           enable_carbons([Alice1, Alice2]),
  154:           client_unsets_presence(Alice1),
  155:           escalus:assert(is_presence_with_type, [<<"unavailable">>],
  156:                          escalus_client:wait_for_stanza(Alice2)),
  157: 
  158:           escalus_client:send(Bob, escalus_stanza:chat_to(Alice2, ?BODY)),
  159:           %% Alice2 receives the message, no carbons for Alice1
  160:           wait_for_message_with_body(Alice2, ?BODY),
  161:           client_sets_presence(Alice1),
  162:           %% still no carbons for Alice1, only presences
  163:           escalus_new_assert:mix_match([is_presence, is_presence],
  164:                                        escalus:wait_for_stanzas(Alice1, 2)),
  165:           escalus:assert(is_presence, [],
  166:                          escalus_client:wait_for_stanza(Alice2)),
  167:           enable_carbons(Alice1),
  168:           %% Send a message with a different body and wait for it.
  169:           %% If we receive it, we can be sure, that our first message has
  170:           %% been fully processed by the routing pipeline.
  171:           Body2 = <<"carbonated">>,
  172:           escalus_client:send(Bob, escalus_stanza:chat_to(Alice2, Body2)),
  173:           wait_for_message_with_body(Alice2, Body2),
  174:           carboncopy_helper:wait_for_carbon_chat_with_body(Alice1, Body2, #{from => Bob, to => Alice2})
  175:       end).
  176: 
  177: dropped_client_doesnt_create_duplicate_carbons(Config) ->
  178:     escalus:fresh_story(
  179:       Config, [{alice, 2}, {bob, 1}],
  180:       fun(Alice1, Alice2, Bob) ->
  181:               enable_carbons([Alice1, Alice2]),
  182:               Msg = escalus_stanza:chat_to(Bob, ?BODY),
  183:               escalus_client:stop(Config, Alice2),
  184:               escalus:assert(is_presence_with_type, [<<"unavailable">>],
  185:                              escalus_client:wait_for_stanza(Alice1)),
  186:               escalus_client:send(Alice1, Msg),
  187:               escalus:assert(is_chat_message, [?BODY],
  188:                              escalus_client:wait_for_stanza(Bob)),
  189:               [] = escalus_client:peek_stanzas(Alice1)
  190:       end).
  191: 
  192: prop_forward_received_chat_messages(Config) ->
  193:     run_prop
  194:       (forward_received,
  195:     ?FORALL({N, Msg}, {no_of_resources(), utterance()},
  196:                true_story
  197:                  (Config, [{alice, 1}, {bob, N}],
  198:                        fun(Users) ->
  199:                            all_bobs_other_resources_get_received_carbons(Users,
  200:                                                                          Msg)
  201:                   end))).
  202: 
  203: prop_forward_sent_chat_messages(Config) ->
  204:     run_prop
  205:         (forward_sent,
  206:     ?FORALL({N, Msg}, {no_of_resources(), utterance()},
  207:                  true_story
  208:                    (Config, [{alice, 1}, {bob, N}],
  209:                        fun(Users) ->
  210:                                all_bobs_other_resources_get_sent_carbons(Users,
  211:                                                                          Msg)
  212:                     end))).
  213: 
  214: prop_normal_routing_to_bare_jid(Config) ->
  215:     run_prop
  216:         (normal_routing,
  217:     ?FORALL({N, Msg}, {no_of_resources(), utterance()},
  218:                  true_story
  219:                    (Config, [{alice, 1}, {bob, N}],
  220:                        fun(Users) ->
  221:                                all_bobs_resources_get_message_to_bare_jid(Users,
  222:                                                                           Msg)
  223:                     end))).
  224: 
  225: chat_message_is_carbon_copied(Config) ->
  226:     message_is_carbon_copied(Config, fun carboncopy_helper:chat_message_with_body/1).
  227: 
  228: normal_message_with_body_is_carbon_copied(Config) ->
  229:     message_is_carbon_copied(Config, fun carboncopy_helper:normal_message_with_body/1).
  230: 
  231: normal_message_with_receipt_is_carbon_copied(Config) ->
  232:     message_is_carbon_copied(Config, fun carboncopy_helper:normal_message_with_receipt/1).
  233: 
  234: normal_message_with_csn_is_carbon_copied(Config) ->
  235:     message_is_carbon_copied(Config, fun carboncopy_helper:normal_message_with_csn/1).
  236: 
  237: normal_message_with_chat_marker_is_carbon_copied(Config) ->
  238:     message_is_carbon_copied(Config, fun carboncopy_helper:normal_message_with_chat_marker/1).
  239: 
  240: message_is_carbon_copied(Config, StanzaFun) ->
  241:     escalus:fresh_story(
  242:         Config, [{alice, 2}, {bob, 1}],
  243:         fun(Alice1, Alice2, Bob) ->
  244:             enable_carbons([Alice1, Alice2]),
  245:             Body = <<"carbonated">>,
  246:             escalus_client:send(Bob, StanzaFun(#{to => Alice2, body => Body})),
  247:             AliceReceived = escalus_client:wait_for_stanza(Alice2),
  248:             escalus:assert(is_message, AliceReceived),
  249:             carboncopy_helper:wait_for_carbon_message(Alice1, #{from => Bob, to => Alice2})
  250:         end).
  251: 
  252: group_chat_is_not_carbon_copied(Config) ->
  253:     escalus:fresh_story(Config, [{alice, 2}, {bob, 1}],
  254:         fun(Alice1, Alice2, Bob) ->
  255:             enable_carbons([Alice1, Alice2]),
  256:             RoomCfg = muc_helper:start_fresh_room(Config, inbox_helper:extract_user_specs(Bob), <<"some_friendly_name">>, default),
  257:             muc_helper:enter_room(RoomCfg, [{Alice1, <<"cool_alice">>}, {Bob, <<"cool_bob">>}]),
  258: 
  259:             Msg = <<"Hi Room!">>,
  260:             muc_helper:send_to_room(RoomCfg, Bob, Msg),
  261:             muc_helper:verify_message_received(RoomCfg, [Alice1, Bob], <<"cool_bob">>, Msg),
  262:             ?assertEqual([], escalus_client:peek_stanzas(Alice2))
  263:       end).
  264: 
  265: local_user_to_muc_participant_is_carbon_copied(Config) ->
  266:     escalus:fresh_story(Config, [{alice, 2}, {bob, 1}],
  267:         fun(Alice1, Alice2, Bob) ->
  268:             enable_carbons([Alice1, Alice2]),
  269:             RoomCfg = muc_helper:start_fresh_room(Config, inbox_helper:extract_user_specs(Bob), <<"some_friendly_name">>, default),
  270:             muc_helper:enter_room(RoomCfg, [{Alice1, <<"cool_alice">>}, {Bob, <<"cool_bob">>}]),
  271:             RoomJid = proplists:get_value(room_jid, RoomCfg),
  272:             Body = <<"Hello!">>,
  273:             Stanza = escalus_stanza:chat_to(<<RoomJid/binary, "/cool_alice">>, Body),
  274: 
  275:             escalus:send(Bob, Stanza),
  276:             escalus:wait_for_stanza(Alice1),
  277:             carboncopy_helper:wait_for_carbon_chat_with_body(Alice2, Body, #{from => <<RoomJid/binary, "/cool_bob">>, to => Alice1})
  278:       end).
  279: 
  280: muc_participant_to_local_user_is_not_carbon_copied(Config) ->
  281:     escalus:fresh_story(Config, [{alice, 2}, {bob, 1}],
  282:         fun(Alice1, Alice2, Bob) ->
  283:             enable_carbons([Alice1, Alice2]),
  284:             RoomCfg = muc_helper:start_fresh_room(Config, inbox_helper:extract_user_specs(Bob), <<"some_friendly_name">>, default),
  285:             muc_helper:enter_room(RoomCfg, [{Alice1, <<"cool_alice">>}, {Bob, <<"cool_bob">>}]),
  286:             RoomJid = proplists:get_value(room_jid, RoomCfg),
  287:             Body = <<"Hello!">>,
  288:             Stanza = escalus_stanza:chat(<<RoomJid/binary, "/cool_bob">>, Alice1, Body),
  289: 
  290:             escalus:send(Bob, Stanza),
  291:             escalus:wait_for_stanza(Alice1),
  292:             ?assertEqual([], escalus_client:peek_stanzas(Alice2))
  293:       end).
  294: 
  295: %%
  296: %% Test scenarios w/assertions
  297: %%
  298: 
  299: all_bobs_resources_get_message_to_bare_jid([Alice, Bob1 | Bobs], Msg) ->
  300:     %% All connected resources receive messages sent
  301:     %% to the user's bare JID without carbon wrappers.
  302:     enable_carbons([Bob1|Bobs]),
  303:     escalus_client:send(
  304:       Alice, escalus_stanza:chat_to(escalus_client:short_jid(Bob1), Msg)),
  305:     GotMsg = fun(BobsResource) ->
  306:                      escalus:assert(
  307:                        is_chat_message,
  308:                        [Msg],
  309:                        escalus_client:wait_for_stanza(BobsResource)),
  310:                      escalus_assert:has_no_stanzas(BobsResource)
  311:              end,
  312:     lists:foreach(GotMsg, [Bob1|Bobs]).
  313: 
  314: all_bobs_other_resources_get_received_carbons([Alice, Bob1 | Bobs], Msg) ->
  315:     enable_carbons([Bob1|Bobs]),
  316:     escalus_client:send(Alice, escalus_stanza:chat_to(Bob1, Msg)),
  317:     escalus_client:wait_for_stanza(Bob1),
  318:     GotForward = fun(BobsResource) ->
  319:                          escalus:assert(
  320:                            is_forwarded_received_message,
  321:                            [escalus_client:full_jid(Alice),
  322:                             escalus_client:full_jid(Bob1),
  323:                             Msg],
  324:                            escalus_client:wait_for_stanza(BobsResource)),
  325:                          escalus_assert:has_no_stanzas(BobsResource) end,
  326:     lists:foreach(GotForward, Bobs).
  327: 
  328: all_bobs_other_resources_get_sent_carbons([Alice, Bob1 | Bobs], Msg) ->
  329:     enable_carbons([Bob1|Bobs]),
  330:     escalus_client:send(Bob1, escalus_stanza:chat_to(Alice, Msg)),
  331:     escalus:assert(is_chat_message, [Msg], escalus_client:wait_for_stanza(Alice)),
  332:     GotCarbon = fun(BobsResource) ->
  333:                         escalus:assert(
  334:                           is_forwarded_sent_message,
  335:                           [escalus_client:full_jid(Bob1),
  336:                            escalus_client:full_jid(Alice),
  337:                            Msg],
  338:                           escalus_client:wait_for_stanza(BobsResource)),
  339:                         escalus_assert:has_no_stanzas(BobsResource) end,
  340:     lists:foreach(GotCarbon, Bobs).
  341: 
  342: %%
  343: %% Internal helpers
  344: %%
  345: 
  346: %% Wrapper around escalus:story. Returns PropEr result.
  347: true_story(Config, UserSpecs, TestFun) ->
  348:     try escalus_fresh:story_with_client_list(Config, UserSpecs, TestFun), true
  349:     catch E ->
  350:               {error, E}
  351:     end.
  352: 
  353: %% Number of resources per users
  354: no_of_resources() -> 1 + rand:uniform(4).
  355: 
  356: %% A sample chat message
  357: utterance() ->
  358:     proper_types:oneof(
  359:       [<<"Now, fair Hippolyta, our nuptial hour">>,
  360:        <<"Draws on apace; four happy days bring in">>,
  361:        <<"Another moon: but, O, methinks, how slow">>,
  362:        <<"This old moon wanes! she lingers my desires">>,
  363:        <<"Like to a step-dame or a dowager">>,
  364:        <<"Long withering out a young man revenue.">>]).
  365: 
  366: 
  367: client_unsets_presence(Client) ->
  368:     escalus_client:send(Client, escalus_stanza:presence(<<"unavailable">>)).
  369: 
  370: client_sets_presence(Client) ->
  371:     escalus_client:send(Client, escalus_stanza:presence(<<"available">>)).
  372: 
  373: run_prop(PropName, Property) ->
  374:     ?AE(true, proper:quickcheck(proper:conjunction([{PropName, Property}]),
  375:                                 [verbose, long_result, {numtests, 3}])).
  376: 
  377: wait_for_message_with_body(Alice, Body) ->
  378:     AliceReceived = escalus_client:wait_for_stanza(Alice),
  379:     escalus:assert(is_chat_message, [Body], AliceReceived).