1: -module(amp_big_SUITE).
    2: %% @doc Tests for XEP-0079 Advanced Message Processing support
    3: %% @reference <a href="http://xmpp.org/extensions/xep-0079.html">XEP-0079</a>
    4: %% @author <simon.zelazny@erlang-solutions.com>
    5: %% @copyright 2014 Erlang Solutions, Ltd.
    6: %% This work was sponsored by Grindr.com
    7: 
    8: -compile([export_all, nowarn_export_all]).
    9: -include_lib("common_test/include/ct.hrl").
   10: -include_lib("escalus/include/escalus.hrl").
   11: -include_lib("escalus/include/escalus_xmlns.hrl").
   12: -include_lib("exml/include/exml.hrl").
   13: 
   14: -import(distributed_helper, [mim/0,
   15:                              require_rpc_nodes/1,
   16:                              rpc/4]).
   17: -import(muc_light_helper, [lbin/1]).
   18: -import(domain_helper, [host_type/0, domain/0]).
   19: 
   20: suite() ->
   21:     require_rpc_nodes([mim]) ++ escalus:suite().
   22: 
   23: all() ->
   24:     [{group, G} || G <- main_group_names(), is_enabled(G)].
   25: 
   26: groups() ->
   27:     group_spec(main_group_names()).
   28: 
   29: is_enabled(mam) -> mongoose_helper:is_rdbms_enabled(host_type());
   30: is_enabled(_) -> true.
   31: 
   32: %% Group definitions
   33: 
   34: main_group_names() ->
   35:     [basic, mam, offline].
   36: 
   37: subgroups(mam) -> [mam_success, mam_failure];
   38: subgroups(offline) -> [offline_success, offline_failure];
   39: subgroups(_) -> [].
   40: 
   41: group_spec(Groups) when is_list(Groups) ->
   42:     lists:flatmap(fun group_spec/1, Groups);
   43: group_spec(Group) ->
   44:     case subgroups(Group) of
   45:         [] -> [{Group, [parallel], test_cases(Group)}];
   46:         SubGroups -> [{Group, [{group, SubG} || SubG <- SubGroups]} | group_spec(SubGroups)]
   47:     end.
   48: 
   49: test_cases(Group) ->
   50:     regular_tests(Group) ++ multiple_config_cth:flatten_and_strip_config(tests_with_config(Group)).
   51: 
   52: regular_tests(basic) -> basic_test_cases();
   53: regular_tests(_) -> [].
   54: 
   55: %% This function is called by multiple_config_cth for each group
   56: %% to get a list of configs for each test case
   57: -spec tests_with_config(_GroupName :: atom()) -> [{TestCase :: atom(),
   58:                                                   [Config :: [{Key :: atom(), Value :: term()}]]}].
   59: tests_with_config(_GroupName) ->
   60:     lists:append([deliver_tests_with_config(notify),
   61:                   deliver_tests_with_config(error),
   62:                   deliver_tests_with_config(drop)]).
   63: 
   64: %% Each of the 'deliver' tests is repeated several times, each time with a different config
   65: deliver_tests_with_config(Action) ->
   66:     multiple_config_cth:add_config(deliver_rule_configs(Action), deliver_test_cases(Action)).
   67: 
   68: %% Each config tests different rules in the AMP message
   69: deliver_rule_configs(Action) ->
   70:     [
   71:      [{rules, [{deliver, direct, Action}]}],
   72:      [{rules, [{deliver, stored, Action}]}],
   73:      [{rules, [{deliver, none, Action}]}],
   74:      [{rules, [{deliver, direct, Action},
   75:                {deliver, stored, Action},
   76:                {deliver, none, Action}]}]
   77:     ].
   78: 
   79: %% Test case list, each test has to be listed exactly once
   80: 
   81: basic_test_cases() ->
   82:     [initial_service_discovery_test,
   83:      actions_and_conditions_discovery_test,
   84: 
   85:      unsupported_actions_test,
   86:      unsupported_conditions_test,
   87:      unacceptable_rules_test,
   88: 
   89:      notify_match_resource_any_test,
   90:      notify_match_resource_exact_test,
   91:      notify_match_resource_other_test,
   92:      notify_match_resource_other_bare_test,
   93: 
   94:      last_rule_applies_test].
   95: 
   96: deliver_test_cases(notify) ->
   97:     [notify_deliver_to_online_user_test,
   98:      notify_deliver_to_online_user_bare_jid_test,
   99:      notify_deliver_to_online_user_recipient_privacy_test,
  100:      notify_deliver_to_offline_user_test,
  101:      notify_deliver_to_offline_user_recipient_privacy_test,
  102:      notify_deliver_to_online_user_broken_connection_test,
  103:      notify_deliver_to_stranger_test,
  104:      notify_deliver_to_unknown_domain_test];
  105: deliver_test_cases(error) ->
  106:     [error_deliver_to_online_user_test,
  107:      error_deliver_to_offline_user_test,
  108:      error_deliver_to_stranger_test];
  109: deliver_test_cases(drop) ->
  110:     [drop_deliver_to_online_user_test,
  111:      drop_deliver_to_offline_user_test,
  112:      drop_deliver_to_stranger_test].
  113: 
  114: %% Setup and teardown
  115: 
  116: init_per_suite(Config) ->
  117:     ConfigWithHooks = [{ct_hooks, [{multiple_config_cth, fun tests_with_config/1}]} | Config],
  118:     {Mod, Code} = rpc(mim(), dynamic_compile, from_string, [amp_test_helper_code()]),
  119:     rpc(mim(), code, load_binary, [Mod, "amp_test_helper.erl", Code]),
  120:     setup_meck(suite),
  121:     escalus:init_per_suite(ConfigWithHooks).
  122: 
  123: amp_test_helper_code() ->
  124:     "-module(amp_test_helper).\n"
  125:     "-compile([export_all, nowarn_export_all]).\n"
  126:     "setup_meck() ->\n"
  127:     "  meck:expect(ranch_tcp, send, fun ranch_tcp_send/2).\n"
  128:     "ranch_tcp_send(Socket, Data) ->\n"
  129:     "  case catch binary:match(Data, <<\"Recipient connection breaks\">>) of\n"
  130:     "    {N, _} when is_integer(N) -> {error, simulated};\n"
  131:     "    _ -> meck:passthrough([Socket, Data])\n"
  132:     "  end.\n".
  133: 
  134: end_per_suite(C) ->
  135:     teardown_meck(suite),
  136:     escalus_fresh:clean(),
  137:     escalus:end_per_suite(C).
  138: 
  139: init_per_group(GroupName, Config) ->
  140:     Config1 = case lists:member(GroupName, main_group_names()) of
  141:                   true ->
  142:                       ConfigWithModules = dynamic_modules:save_modules(host_type(), Config),
  143:                       dynamic_modules:ensure_modules(host_type(), required_modules(GroupName)),
  144:                       ConfigWithModules;
  145:                   false ->
  146:                       Config
  147:               end,
  148:     setup_meck(GroupName),
  149:     save_offline_status(GroupName, Config1).
  150: 
  151: setup_meck(suite) ->
  152:     ok = rpc(mim(), meck, new, [ranch_tcp, [passthrough, no_link]]),
  153:     ok = rpc(mim(), amp_test_helper, setup_meck, []);
  154: setup_meck(mam_failure) ->
  155:     ok = rpc(mim(), meck, expect, [mod_mam_rdbms_arch, archive_message, 3, {ok, {error, simulated}}]);
  156: setup_meck(offline_failure) ->
  157:     ok = rpc(mim(), meck, expect, [mod_offline_backend_module(), write_messages, 4, {error, simulated}]);
  158: setup_meck(_) -> ok.
  159: 
  160: save_offline_status(mam_success, Config) -> [{offline_storage, mam} | Config];
  161: save_offline_status(mam_failure, Config) -> [{offline_storage, mam_failure} | Config];
  162: save_offline_status(offline_success, Config) -> [{offline_storage, offline} | Config];
  163: save_offline_status(offline_failure, Config) -> [{offline_storage, offline_failure} | Config];
  164: save_offline_status(basic, Config) -> [{offline_storage, none} | Config];
  165: save_offline_status(_GN, Config) -> Config.
  166: 
  167: end_per_group(GroupName, Config) ->
  168:     teardown_meck(GroupName),
  169:     case lists:member(GroupName, main_group_names()) of
  170:         true -> dynamic_modules:restore_modules(Config);
  171:         false -> ok
  172:     end.
  173: 
  174: teardown_meck(mam_failure) ->
  175:     rpc(mim(), meck, unload, [mod_mam_rdbms_arch]);
  176: teardown_meck(offline_failure) ->
  177:     rpc(mim(), meck, unload, [mod_offline_backend_module()]);
  178: teardown_meck(suite) ->
  179:     rpc(mim(), meck, unload, []);
  180: teardown_meck(_) -> ok.
  181: 
  182: init_per_testcase(Name, C) -> escalus:init_per_testcase(Name, C).
  183: end_per_testcase(Name, C) -> escalus:end_per_testcase(Name, C).
  184: 
  185: %% Test cases
  186: 
  187: initial_service_discovery_test(Config) ->
  188:     escalus:fresh_story(
  189:       Config, [{alice, 1}],
  190:       fun(Alice) ->
  191:               escalus_client:send(Alice, disco_info()),
  192:               Response = escalus_client:wait_for_stanza(Alice),
  193:               escalus:assert(has_feature, [ns_amp()], Response)
  194:       end).
  195: 
  196: actions_and_conditions_discovery_test(Config) ->
  197:     escalus:fresh_story(
  198:       Config, [{alice, 1}],
  199:       fun(Alice) ->
  200:           Args = [ns_amp(),
  201:                   <<"http://jabber.org/protocol/amp?action=notify">>,
  202:                   <<"http://jabber.org/protocol/amp?action=error">>,
  203:                   <<"http://jabber.org/protocol/amp?condition=deliver">>,
  204:                   <<"http://jabber.org/protocol/amp?condition=match-resource">>
  205:                   ],
  206:           escalus_client:send(Alice, disco_info_amp_node()),
  207:           Response = escalus_client:wait_for_stanza(Alice),
  208:           assert_has_features(Response, Args)
  209:       end).
  210: 
  211: 
  212: unsupported_actions_test(Config) ->
  213:     escalus:fresh_story(
  214:       Config, [{alice, 1}, {bob, 1}],
  215:       fun(Alice, Bob) ->
  216:               %% given
  217:               Msg = amp_message_to(Bob, [{deliver, direct, alert}],  % alert is unsupported
  218:                                    <<"A paradoxical payload!">>),
  219:               %% when
  220:               client_sends_message(Alice, Msg),
  221: 
  222:               % then
  223:               client_receives_amp_error(Alice, {deliver, direct, alert}, <<"unsupported-actions">>)
  224:       end).
  225: 
  226: unsupported_conditions_test(Config) ->
  227:     escalus:fresh_story(
  228:       Config, [{alice, 1}, {bob, 1}],
  229:       fun(Alice, Bob) ->
  230:               %% given
  231:               %% expire-at is unsupported
  232:               Msg = amp_message_to(Bob, [{'expire-at', <<"2020-06-06T12:20:20Z">>, notify}],
  233:                                    <<"Never fade away!">>),
  234:               %% when
  235:               client_sends_message(Alice, Msg),
  236: 
  237:               % then
  238:               client_receives_amp_error(Alice, {'expire-at', <<"2020-06-06T12:20:20Z">>, notify},
  239:                                    <<"unsupported-conditions">>)
  240:       end).
  241: 
  242: unacceptable_rules_test(Config) ->
  243:     escalus:fresh_story(
  244:       Config, [{alice, 1}, {bob, 1}],
  245:       fun(Alice, Bob) ->
  246:               %% given
  247:               Msg = amp_message_to(Bob, [{broken, rule, spec}
  248:                                         , {also_broken, rule, spec}
  249:                                         ],
  250:                                    <<"Break all the rules!">>),
  251:               %% when
  252:               client_sends_message(Alice, Msg),
  253: 
  254:               % then
  255:               client_receives_amp_error(Alice, [{broken, rule, spec}
  256:                                            , {also_broken, rule, spec}],
  257:                                     <<"not-acceptable">>)
  258:       end).
  259: 
  260: notify_deliver_to_online_user_test(Config) ->
  261:     escalus:fresh_story(
  262:       Config, [{alice, 1}, {bob, 1}],
  263:       fun(Alice, Bob) ->
  264:               %% given
  265:               Rule = {deliver, direct, notify},
  266:               Rules = rules(Config, [Rule]),
  267:               Msg = amp_message_to(Bob, Rules, <<"I want to be sure you get this!">>),
  268: 
  269:               %% when
  270:               client_sends_message(Alice, Msg),
  271: 
  272:               % then
  273:               case lists:member(Rule, Rules) of
  274:                   true -> client_receives_notification(Alice, Bob, Rule);
  275:                   false -> ok
  276:               end,
  277:               client_receives_message(Bob, <<"I want to be sure you get this!">>),
  278:               client_receives_nothing(Alice)
  279:       end).
  280: 
  281: 
  282: 
  283: notify_deliver_to_online_user_bare_jid_test(Config) ->
  284:     escalus:fresh_story(
  285:       Config, [{alice, 1}, {bob, 1}],
  286:       fun(Alice, Bob) ->
  287:               %% given
  288:               Message = <<"One of your resources needs to get this!">>,
  289:               Rule = {deliver, direct, notify},
  290:               Rules = rules(Config, [Rule]),
  291:               BobsBareJid = escalus_client:short_jid(Bob),
  292:               Msg = amp_message_to(BobsBareJid, Rules, Message),
  293:               %% when
  294:               client_sends_message(Alice, Msg),
  295:               % then
  296:               case lists:member(Rule, Rules) of
  297:                   true -> client_receives_notification(Alice, BobsBareJid, Rule);
  298:                   false -> ok
  299:               end,
  300:               client_receives_message(Bob, Message),
  301:               client_receives_nothing(Alice)
  302:       end).
  303: 
  304: notify_deliver_to_online_user_recipient_privacy_test(Config) ->
  305:     case is_module_loaded(mod_mam_pm) of
  306:         true -> {skip, "MAM does not support privacy lists"};
  307:         false -> do_notify_deliver_to_online_user_recipient_privacy_test(Config)
  308:     end.
  309: 
  310: do_notify_deliver_to_online_user_recipient_privacy_test(Config) ->
  311:     escalus:fresh_story(
  312:       Config, [{alice, 1}, {bob, 1}],
  313:       fun(Alice, Bob) ->
  314:               %% given
  315:               Rule = {deliver, none, notify},
  316:               Rules = rules(Config, [Rule]),
  317:               Msg = amp_message_to(Bob, Rules, <<"Should be filtered by Bob's privacy list">>),
  318:               privacy_helper:set_and_activate(Bob, <<"deny_all_message">>),
  319: 
  320:               %% when
  321:               client_sends_message(Alice, Msg),
  322: 
  323:               % then
  324:               case lists:member(Rule, Rules) of
  325:                   true -> client_receives_notification(Alice, Bob, Rule);
  326:                   false -> ok
  327:               end,
  328:               client_receives_generic_error(Alice, <<"503">>, <<"cancel">>),
  329:               client_receives_nothing(Alice),
  330:               client_receives_nothing(Bob)
  331:       end).
  332: 
  333: notify_deliver_to_online_user_broken_connection_test(Config) ->
  334:     escalus:fresh_story(
  335:       Config, [{alice, 1}, {bob, 1}],
  336:       fun(Alice, Bob) ->
  337:               %% given
  338:               Rule = {deliver, case ?config(offline_storage, Config) of
  339:                                    mam -> stored;
  340:                                    _ -> none
  341:                                end, notify},
  342:               Rules = rules(Config, [Rule]),
  343:               %% This special message is matched by the
  344:               %% amp_test_helper:ranch_tcp_send/2 mock,
  345:               %% (see amp_test_helper_code/0)
  346:               Msg = amp_message_to(Bob, Rules, <<"Recipient connection breaks">>),
  347: 
  348:               %% when
  349:               client_sends_message(Alice, Msg),
  350: 
  351:               % then
  352:               case lists:member(Rule, Rules) of
  353:                   true -> client_receives_notification(Alice, Bob, Rule);
  354:                   false -> ok
  355:               end,
  356:               client_receives_nothing(Alice),
  357: 
  358:               %% Kill Bob's connection to avoid errors with closing the stream
  359:               %% while the session is being resumed after the simulated error
  360:               escalus_connection:kill(Bob)
  361:       end),
  362:     ok.
  363: 
  364: notify_deliver_to_offline_user_test(Config) ->
  365:     FreshConfig = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  366:     escalus:story(
  367:       FreshConfig, [{alice, 1}],
  368:       fun(Alice) ->
  369:               %% given
  370:               Rule = {deliver, case is_offline_storage_working(Config) of
  371:                                    true -> stored;
  372:                                    false -> none
  373:                                end, notify},
  374:               Rules = rules(Config, [Rule]),
  375:               BobJid = escalus_users:get_jid(FreshConfig, bob),
  376:               Msg = amp_message_to(BobJid, Rules, <<"A message in a bottle...">>),
  377: 
  378:               %% when
  379:               client_sends_message(Alice, Msg),
  380: 
  381:               % then
  382:               case lists:member(Rule, Rules) of
  383:                   true -> client_receives_notification(Alice, BobJid, Rule);
  384:                   false -> ok
  385:               end,
  386:               case ?config(offline_storage, Config) of
  387:                   offline_failure -> client_receives_generic_error(Alice, <<"500">>, <<"wait">>);
  388:                   _ -> client_receives_nothing(Alice)
  389:               end
  390:       end),
  391:     wait_until_no_session(FreshConfig, alice),
  392:     case is_offline_storage_working(Config) of
  393:         true -> user_has_incoming_offline_message(FreshConfig, bob, <<"A message in a bottle...">>);
  394:         false -> user_has_no_incoming_offline_messages(FreshConfig, bob)
  395:     end.
  396: 
  397: is_offline_storage_working(Config) ->
  398:     Status = ?config(offline_storage, Config),
  399:     Status == mam orelse Status == offline.
  400: 
  401: notify_deliver_to_offline_user_recipient_privacy_test(Config) ->
  402:     case is_module_loaded(mod_mam_pm) of
  403:         true -> {skip, "MAM does not support privacy lists"};
  404:         false -> do_notify_deliver_to_offline_user_recipient_privacy_test(Config)
  405:     end.
  406: 
  407: do_notify_deliver_to_offline_user_recipient_privacy_test(Config) ->
  408:     FreshConfig = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  409:     escalus:story(
  410:       FreshConfig, [{alice, 1}, {bob, 1}],
  411:       fun(Alice, Bob) ->
  412: 
  413:               privacy_helper:set_and_activate(Bob, <<"deny_all_message">>),
  414:               privacy_helper:set_default_list(Bob, <<"deny_all_message">>),
  415:               mongoose_helper:logout_user(Config, Bob),
  416:               %% given
  417:               Rule = {deliver, none, notify},
  418:               Rules = rules(Config, [Rule]),
  419:               BobJid = lbin(escalus_client:short_jid(Bob)),
  420: 
  421:               Msg = amp_message_to(BobJid, Rules, <<"A message in a bottle...">>),
  422: 
  423:               %% when
  424:               client_sends_message(Alice, Msg),
  425: 
  426:               % then
  427:               case lists:member(Rule, Rules) of
  428:                   true -> client_receives_notification(Alice, BobJid, Rule);
  429:                   false -> ok
  430:               end,
  431:               client_receives_nothing(Alice)
  432:       end),
  433:     user_has_no_incoming_offline_messages(FreshConfig, bob).
  434: 
  435: notify_deliver_to_stranger_test(Config) ->
  436:     escalus:fresh_story(
  437:       Config, [{alice, 1}],
  438:       fun(Alice) ->
  439:               %% given
  440:               Rule = {deliver, none, notify},
  441:               Rules = rules(Config, [Rule]),
  442:               Domain = domain(),
  443:               StrangerJid = <<"stranger@", Domain/binary>>,
  444:               Msg = amp_message_to(StrangerJid, Rules, <<"A message in a bottle...">>),
  445: 
  446:               %% when
  447:               client_sends_message(Alice, Msg),
  448: 
  449:               % then
  450:               case lists:member(Rule, Rules) of
  451:                   true -> client_receives_notification(Alice, StrangerJid, Rule);
  452:                   false -> ok
  453:               end,
  454:               client_receives_generic_error(Alice, <<"503">>, <<"cancel">>),
  455:               client_receives_nothing(Alice)
  456:       end).
  457: 
  458: notify_deliver_to_unknown_domain_test(Config) ->
  459:     escalus:fresh_story(
  460:       Config, [{alice, 1}],
  461:       fun(Alice) ->
  462:               %% given
  463:               StrangerJid = <<"stranger@unknown.domain">>,
  464:               Rule = {deliver, none, notify},
  465:               Rules = rules(Config, [Rule]),
  466:               Msg = amp_message_to(StrangerJid, Rules, <<"Msg to unknown domain">>),
  467:               %% when
  468:               client_sends_message(Alice, Msg),
  469: 
  470:               % then
  471:               case lists:member(Rule, Rules) of
  472:                   true -> client_receives_notification(Alice, StrangerJid, Rule);
  473:                   false -> ok
  474:               end,
  475:               % error 404: 'remote server not found' is expected
  476:               client_receives_generic_error(Alice, <<"404">>, <<"cancel">>),
  477:               client_receives_nothing(Alice)
  478:       end).
  479: 
  480: notify_match_resource_any_test(Config) ->
  481:     escalus:fresh_story(
  482:       Config, [{alice, 1}, {bob, 4}],
  483:       fun(Alice, Bob, _, _, _) ->
  484:               %% given
  485:               Msg = amp_message_to(Bob, [{'match-resource', any, notify}],
  486:                                    <<"Church-encoded hot-dogs">>),
  487:               %% when
  488:               client_sends_message(Alice, Msg),
  489: 
  490:               % then
  491:               client_receives_notification(Alice, Bob, {'match-resource', any, notify}),
  492:               client_receives_message(Bob, <<"Church-encoded hot-dogs">>)
  493:       end).
  494: 
  495: notify_match_resource_exact_test(Config) ->
  496:     escalus:fresh_story(
  497:       Config, [{alice, 1}, {bob, 4}],
  498:       fun(Alice, _, _, Bob3, _) ->
  499:               %% given
  500:               Msg = amp_message_to(Bob3, [{'match-resource', exact, notify}],
  501:                                    <<"Resource three, your battery is on fire!">>),
  502:               %% when
  503:               client_sends_message(Alice, Msg),
  504: 
  505:               % then
  506:               client_receives_notification(Alice, Bob3, {'match-resource', exact, notify}),
  507:               client_receives_message(Bob3, <<"Resource three, your battery is on fire!">>)
  508:       end).
  509: 
  510: notify_match_resource_other_test(Config) ->
  511:     escalus:fresh_story(
  512:       Config, [{alice, 1}, {bob, 1}],
  513:       fun(Alice, Bob) ->
  514:               %% given
  515:               NonmatchingJid = << (escalus_client:short_jid(Bob))/binary,
  516:                                   "/blahblahblah_resource" >>,
  517:               Msg = amp_message_to(NonmatchingJid,
  518:                                    [{'match-resource', other, notify}],
  519:                                     <<"A Bob by any other name!">>),
  520: 
  521:               %% when
  522:               client_sends_message(Alice, Msg),
  523: 
  524:               % then
  525:               client_receives_notification(Alice, NonmatchingJid,
  526:                                            {'match-resource', other, notify}),
  527:               client_receives_message(Bob, <<"A Bob by any other name!">>)
  528:       end).
  529: 
  530: notify_match_resource_other_bare_test(Config) ->
  531:     escalus:fresh_story(
  532:       Config, [{alice, 1}, {bob, 1}],
  533:       fun(Alice, Bob) ->
  534:               %% given
  535:               BareJid = escalus_client:short_jid(Bob),
  536:               Msg = amp_message_to(BareJid,
  537:                                    [{'match-resource', other, notify}],
  538:                                     <<"A Bob by any other name!">>),
  539: 
  540:               %% when
  541:               client_sends_message(Alice, Msg),
  542: 
  543:               % then
  544:               client_receives_notification(Alice, BareJid, {'match-resource', other, notify}),
  545:               client_receives_message(Bob, <<"A Bob by any other name!">>)
  546:       end).
  547: 
  548: error_deliver_to_online_user_test(Config) ->
  549:     escalus:fresh_story(
  550:       Config, [{alice, 1}, {bob, 1}],
  551:       fun(Alice, Bob) ->
  552:               %% given
  553:               Rule = {deliver, direct, error},
  554:               Rules = rules(Config, [Rule]),
  555:               Msg = amp_message_to(Bob, Rules, <<"It might cause an error">>),
  556: 
  557:               %% when
  558:               client_sends_message(Alice, Msg),
  559: 
  560:               % then
  561:               case lists:member(Rule, Rules) of
  562:                   true ->
  563:                       client_receives_amp_error(Alice, Bob, Rule, <<"undefined-condition">>),
  564:                       client_receives_nothing(Bob);
  565:                   false ->
  566:                       client_receives_message(Bob, <<"It might cause an error">>)
  567:               end,
  568:               client_receives_nothing(Alice)
  569:       end).
  570: 
  571: error_deliver_to_offline_user_test(Config) ->
  572:     FreshConfig = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  573:     Rule = {deliver, case ?config(offline_storage, Config) of
  574:                          none -> none;
  575:                          _ -> stored
  576:                      end, error},
  577:     Rules = rules(Config, [Rule]),
  578:     escalus:story(
  579:       FreshConfig, [{alice, 1}],
  580:       fun(Alice) ->
  581:               %% given
  582:               BobJid = escalus_users:get_jid(FreshConfig, bob),
  583:               Msg = amp_message_to(BobJid, Rules, <<"A message in a bottle...">>),
  584: 
  585:               %% when
  586:               client_sends_message(Alice, Msg),
  587: 
  588:               % then
  589:               case lists:member(Rule, Rules) of
  590:                   true ->
  591:                       client_receives_amp_error(Alice, BobJid, Rule, <<"undefined-condition">>);
  592:                   false ->
  593:                       check_offline_storage(Alice, Config)
  594:               end
  595:       end),
  596:     wait_until_no_session(FreshConfig, alice),
  597:     case is_offline_storage_working(Config) andalso not lists:member(Rule, Rules) of
  598:         true -> user_has_incoming_offline_message(FreshConfig, bob, <<"A message in a bottle...">>);
  599:         false -> user_has_no_incoming_offline_messages(FreshConfig, bob)
  600:     end.
  601: 
  602: check_offline_storage(User, Config) ->
  603:     case ?config(offline_storage, Config) of
  604:         offline_failure ->
  605:             client_receives_generic_error(User, <<"500">>, <<"wait">>);
  606:         _ -> client_receives_nothing(User)
  607:     end.
  608: 
  609: error_deliver_to_stranger_test(Config) ->
  610:     escalus:fresh_story(
  611:       Config, [{alice, 1}],
  612:       fun(Alice) ->
  613:               %% given
  614:               Rule = {deliver, none, error},
  615:               Rules = rules(Config, [Rule]),
  616:               Domain = domain(),
  617:               StrangerJid = <<"stranger@", Domain/binary>>,
  618:               Msg = amp_message_to(StrangerJid, Rules, <<"This cannot possibly succeed">>),
  619: 
  620:               %% when
  621:               client_sends_message(Alice, Msg),
  622: 
  623:               % then
  624:               case lists:member(Rule, Rules) of
  625:                   true -> client_receives_amp_error(Alice, StrangerJid, Rule,
  626:                                                     <<"undefined-condition">>);
  627:                   false -> client_receives_generic_error(Alice, <<"503">>, <<"cancel">>)
  628:               end,
  629:               client_receives_nothing(Alice)
  630: 
  631:       end).
  632: 
  633: drop_deliver_to_online_user_test(Config) ->
  634:     escalus:fresh_story(
  635:       Config, [{alice, 1}, {bob, 1}],
  636:       fun(Alice, Bob) ->
  637:               %% given
  638:               Rule = {deliver, direct, drop},
  639:               Rules = rules(Config, [Rule]),
  640:               Msg = amp_message_to(Bob, Rules, <<"It might get dropped">>),
  641: 
  642:               %% when
  643:               client_sends_message(Alice, Msg),
  644: 
  645:               % then
  646:               case lists:member(Rule, Rules) of
  647:                   true -> client_receives_nothing(Bob);
  648:                   false -> client_receives_message(Bob, <<"It might get dropped">>)
  649:               end,
  650:               client_receives_nothing(Alice)
  651:       end).
  652: 
  653: drop_deliver_to_offline_user_test(Config) ->
  654:     FreshConfig = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  655:     Rule = {deliver, case ?config(offline_storage, Config) of
  656:                          none -> none;
  657:                          _ -> stored
  658:                      end, drop},
  659:     Rules = rules(Config, [Rule]),
  660:     Message = <<"A message in a bottle...">>,
  661:     escalus:story(
  662:       FreshConfig, [{alice, 1}],
  663:       fun(Alice) ->
  664:               %% given
  665:               BobJid = escalus_users:get_jid(FreshConfig, bob),
  666:               Msg = amp_message_to(BobJid, Rules, Message),
  667: 
  668:               %% when
  669:               client_sends_message(Alice, Msg),
  670: 
  671:               % then
  672:               case lists:member(Rule, Rules) orelse
  673:                    ?config(offline_storage, Config) /= offline_failure of
  674:                   true -> client_receives_nothing(Alice);
  675:                   false -> client_receives_generic_error(Alice, <<"500">>, <<"wait">>)
  676:               end
  677:       end),
  678:     wait_until_no_session(FreshConfig, alice),
  679:     case is_offline_storage_working(Config) andalso not lists:member(Rule, Rules) of
  680:         true -> user_has_incoming_offline_message(FreshConfig, bob, Message);
  681:         false -> user_has_no_incoming_offline_messages(FreshConfig, bob)
  682:     end.
  683: 
  684: drop_deliver_to_stranger_test(Config) ->
  685:     escalus:fresh_story(
  686:       Config, [{alice, 1}],
  687:       fun(Alice) ->
  688:               %% given
  689:               Rule = {deliver, none, drop},
  690:               Rules = rules(Config, [Rule]),
  691:               Domain = domain(),
  692:               StrangerJid = <<"stranger@", Domain/binary>>,
  693:               Msg = amp_message_to(StrangerJid, Rules, <<"This cannot possibly succeed">>),
  694: 
  695:               %% when
  696:               client_sends_message(Alice, Msg),
  697: 
  698:               % then
  699:               case lists:member(Rule, Rules) of
  700:                   true -> ok;
  701:                   false -> client_receives_generic_error(Alice, <<"503">>, <<"cancel">>)
  702:               end,
  703:               client_receives_nothing(Alice)
  704: 
  705:       end).
  706: 
  707: last_rule_applies_test(Config) ->
  708:     escalus:fresh_story(
  709:       Config, [{alice, 1}, {bob, 1}],
  710:       fun(Alice, Bob) ->
  711:               %% given
  712:               BobsBareJid = escalus_client:short_jid(Bob),
  713:               Msg = amp_message_to(BobsBareJid, [{deliver, none, error},
  714:                                                  {deliver, stored, error},
  715:                                                  {deliver, direct, notify}],
  716:                                    <<"One of your resources needs to get this!">>),
  717:               %% when
  718:               client_sends_message(Alice, Msg),
  719: 
  720:               % then
  721:               client_receives_notification(Alice, BobsBareJid, {deliver, direct, notify}),
  722:               client_receives_message(Bob, <<"One of your resources needs to get this!">>)
  723:       end).
  724: 
  725: %% Internal
  726: 
  727: wait_until_no_session(FreshConfig, User) ->
  728:     U = escalus_users:get_username(FreshConfig, User),
  729:     S = escalus_users:get_server(FreshConfig, User),
  730:     JID = jid:make(U, S, <<>>),
  731:     mongoose_helper:wait_until(
  732:       fun() -> rpc(mim(), ejabberd_sm, get_user_resources, [JID]) end, []).
  733: 
  734: user_has_no_incoming_offline_messages(FreshConfig, UserName) ->
  735:     escalus:fresh_story(
  736:       FreshConfig, [{UserName, 1}],
  737:       fun(User) ->
  738:               client_receives_nothing(User),
  739:               case is_module_loaded(mod_mam_pm) of
  740:                   true -> client_has_no_mam_messages(User);
  741:                   false -> ok
  742:               end
  743:       end).
  744: 
  745: user_has_incoming_offline_message(FreshConfig, UserName, MsgText) ->
  746:     true = is_module_loaded(mod_mam_pm) orelse is_module_loaded(mod_offline),
  747:     {ok, Client} = escalus_client:start(FreshConfig, UserName, <<"new-session">>),
  748:     escalus:send(Client, escalus_stanza:presence(<<"available">>)),
  749:     case is_module_loaded(mod_offline) of
  750:         true -> client_receives_message(Client, MsgText);
  751:         false -> ok
  752:     end,
  753:     Presence = escalus:wait_for_stanza(Client),
  754:     escalus:assert(is_presence, Presence),
  755:     case is_module_loaded(mod_mam_pm) of
  756:         true -> client_has_mam_message(Client);
  757:         false -> ok
  758:     end,
  759:     escalus_client:stop(FreshConfig, Client).
  760: 
  761: client_has_no_mam_messages(User) ->
  762:     P = mam_helper:mam04_props(),
  763:     escalus:send(User, mam_helper:stanza_archive_request(P, <<"q1">>)),
  764:     Res = mam_helper:wait_archive_respond(User),
  765:     mam_helper:assert_respond_size(0, Res).
  766: 
  767: client_has_mam_message(User) ->
  768:     P = mam_helper:mam04_props(),
  769:     escalus:send(User, mam_helper:stanza_archive_request(P, <<"q1">>)),
  770:     Res = mam_helper:wait_archive_respond(User),
  771:     mam_helper:assert_respond_size(1, Res).
  772: 
  773: rules(Config, Default) ->
  774:     case lists:keysearch(rules, 1, Config) of
  775:         {value, {rules, Val}} -> Val;
  776:         _ -> Default
  777:     end.
  778: 
  779: ns_amp() ->
  780:     <<"http://jabber.org/protocol/amp">>.
  781: 
  782: client_sends_message(Client, Msg) ->
  783:     escalus_client:send(Client, Msg).
  784: 
  785: client_receives_amp_error(Client, Rules, AmpErrorKind) when is_list(Rules) ->
  786:     Received = escalus_client:wait_for_stanza(Client),
  787:     assert_amp_error(Client, Received, Rules, AmpErrorKind);
  788: client_receives_amp_error(Client, Rule, AmpErrorKind) ->
  789:     client_receives_amp_error(Client, [Rule], AmpErrorKind).
  790: 
  791: client_receives_amp_error(Client, IntendedRecipient, Rule, AmpErrorKind) ->
  792:     Received = escalus_client:wait_for_stanza(Client),
  793:     assert_amp_error_with_full_amp(Client, IntendedRecipient,
  794:                                    Received, Rule, AmpErrorKind).
  795: 
  796: client_receives_generic_error(Client, Code, Type) ->
  797:     Received = escalus_client:wait_for_stanza(Client, 5000),
  798:     escalus:assert(fun contains_error/3, [Code, Type], Received).
  799: 
  800: client_receives_nothing(Client) ->
  801:     timer:sleep(300),
  802:     escalus_assert:has_no_stanzas(Client).
  803: 
  804: client_receives_message(Client, MsgText) ->
  805:     Received = escalus_client:wait_for_stanza(Client),
  806:     escalus:assert(is_chat_message, [MsgText], Received).
  807: 
  808: client_receives_notification(Client, IntendedRecipient, Rule) ->
  809:     Msg = escalus_client:wait_for_stanza(Client),
  810:     assert_notification(Client, IntendedRecipient, Msg, Rule).
  811: 
  812: disco_info() ->
  813:     escalus_stanza:disco_info(domain()).
  814: 
  815: disco_info_amp_node() ->
  816:     escalus_stanza:disco_info(domain(), ns_amp()).
  817: 
  818: assert_amp_error(Client, Response, Rules, AmpErrorKind) when is_list(Rules) ->
  819:     ClientJID = escalus_client:full_jid(Client),
  820:     Server = escalus_client:server(Client),
  821:     Server = exml_query:attr(Response, <<"from">>),
  822:     ClientJID = exml_query:attr(Response, <<"to">>),
  823:     escalus:assert(fun contains_amp/5,
  824:                    [amp_status_attr(AmpErrorKind), no_to_attr, no_from_attr, Rules],
  825:                    Response),
  826:     escalus:assert(fun contains_amp_error/3,
  827:                    [AmpErrorKind, Rules],
  828:                    Response);
  829: 
  830: assert_amp_error(Client, Response, Rule, AmpErrorKind) ->
  831:     assert_amp_error(Client, Response, [Rule], AmpErrorKind).
  832: 
  833: assert_amp_error_with_full_amp(Client, IntendedRecipient, Response,
  834:                                {_C, _V, _A} = Rule, AmpErrorKind) ->
  835:     ClientJID = escalus_client:full_jid(Client),
  836:     RecipientJID = full_jid(IntendedRecipient),
  837:     Server = escalus_client:server(Client),
  838:     Server = exml_query:attr(Response, <<"from">>),
  839:     ClientJID = exml_query:attr(Response, <<"to">>),
  840:     escalus:assert(fun contains_amp/5,
  841:                    [amp_status_attr(AmpErrorKind), RecipientJID, ClientJID, [Rule]],
  842:                    Response),
  843:     escalus:assert(fun contains_amp_error/3,
  844:                    [AmpErrorKind, [Rule]],
  845:                    Response).
  846: 
  847: 
  848: assert_notification(Client, IntendedRecipient, Response, {_C, _V, A} = Rule) ->
  849:     ClientJID = escalus_client:full_jid(Client),
  850:     RecipientJID = full_jid(IntendedRecipient),
  851:     Server = escalus_client:server(Client),
  852:     Action = a2b(A),
  853:     Server = exml_query:attr(Response, <<"from">>),
  854:     ClientJID = exml_query:attr(Response, <<"to">>),
  855:     escalus:assert(fun contains_amp/5,
  856:                    [Action, RecipientJID, ClientJID, [Rule]],
  857:                    Response).
  858: 
  859: assert_has_features(Response, Features) ->
  860:     CheckF = fun(F) -> escalus:assert(has_feature, [F], Response) end,
  861:     lists:foreach(CheckF, Features).
  862: 
  863: full_jid(#client{} = Client) ->
  864:     escalus_client:full_jid(Client);
  865: full_jid(B) when is_binary(B) ->
  866:     B.
  867: 
  868: %% @TODO: Move me out to escalus_stanza %%%%%%%%%
  869: %%%%%%%%% Element constructors %%%%%%%%%%%%%%%%%%
  870: amp_message_to(To, Rules, MsgText) ->
  871:     Msg0 = #xmlel{children=C} = escalus_stanza:chat_to(To, MsgText),
  872:     Msg = escalus_stanza:set_id(Msg0, escalus_stanza:id()),
  873:     Amp = amp_el(Rules),
  874:     Msg#xmlel{children = C ++ [Amp]}.
  875: 
  876: amp_el([]) ->
  877:     throw("cannot build <amp> with no rules!");
  878: amp_el(Rules) ->
  879:     #xmlel{name = <<"amp">>,
  880:            attrs = [{<<"xmlns">>, ns_amp()}],
  881:            children = [ rule_el(R) || R <- Rules ]}.
  882: 
  883: rule_el({Condition, Value, Action}) ->
  884:     check_rules(Condition, Value, Action),
  885:     #xmlel{name = <<"rule">>
  886:           , attrs = [{<<"condition">>, a2b(Condition)}
  887:                     , {<<"value">>, a2b(Value)}
  888:                     , {<<"action">>, a2b(Action)}]}.
  889: 
  890: %% @TODO: Move me out to escalus_pred %%%%%%%%%%%%
  891: %%%%%%%%% XML predicates %%%%% %%%%%%%%%%%%%%%%%%%
  892: contains_amp(Status, To, From, ExpectedRules, Stanza) when is_list(ExpectedRules)->
  893:     Amp = exml_query:subelement(Stanza, <<"amp">>),
  894:     undefined =/= Amp andalso
  895:         To == exml_query:attr(Amp, <<"to">>, no_to_attr) andalso
  896:         From == exml_query:attr(Amp, <<"from">>, no_from_attr) andalso
  897:         Status == exml_query:attr(Amp, <<"status">>, no_status_attr) andalso
  898:         all_present([ rule_el(R) || R <- ExpectedRules ], exml_query:subelements(Amp, <<"rule">>)).
  899: 
  900: contains_amp_error(AmpErrorKind, Rules, Response) ->
  901:     ErrorEl = exml_query:subelement(Response, <<"error">>),
  902:     <<"modify">> == exml_query:attr(ErrorEl, <<"type">>)
  903:         andalso
  904:         amp_error_code(AmpErrorKind) == exml_query:attr(ErrorEl, <<"code">>)
  905:         andalso
  906:         undefined =/= (Marker = exml_query:subelement(ErrorEl, amp_error_marker(AmpErrorKind)))
  907:         andalso
  908:         ns_stanzas() == exml_query:attr(Marker, <<"xmlns">>)
  909:         andalso
  910:         undefined =/= (Container = exml_query:subelement(ErrorEl,
  911:                                             amp_error_container(AmpErrorKind)))
  912:         andalso
  913:         all_present([ rule_el(R) || R <- Rules ], exml_query:subelements(Container, <<"rule">>)).
  914: 
  915: contains_error(Code, Type, Response) ->
  916:     ErrorEl = exml_query:subelement(Response, <<"error">>),
  917:     Type == exml_query:attr(ErrorEl, <<"type">>)
  918:         andalso (Code == any orelse Code == exml_query:attr(ErrorEl, <<"code">>)).
  919: 
  920: all_present(Needles, Haystack) ->
  921:     list_and([ lists:member(Needle, Haystack)
  922:                || Needle <- Needles ]).
  923: 
  924: 
  925: list_and(List) ->
  926:     lists:all(fun(X) -> X =:= true end, List).
  927: 
  928: ns_stanzas() ->
  929:     <<"urn:ietf:params:xml:ns:xmpp-stanzas">>.
  930: 
  931: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  932: 
  933: check_rules(deliver, direct, notify) -> ok;
  934: check_rules(deliver, stored, notify) -> ok;
  935: check_rules(deliver, none, notify) -> ok;
  936: 
  937: check_rules(deliver, direct, error) -> ok;
  938: check_rules(deliver, stored, error) -> ok;
  939: check_rules(deliver, none, error) -> ok;
  940: 
  941: check_rules(deliver, direct, drop) -> ok;
  942: check_rules(deliver, stored, drop) -> ok;
  943: check_rules(deliver, none, drop) -> ok;
  944: 
  945: check_rules('match-resource', any, notify) -> ok;
  946: check_rules('match-resource', exact, notify) -> ok;
  947: check_rules('match-resource', other, notify) -> ok;
  948: 
  949: check_rules(deliver, direct, alert) -> ok;       %% for testing unsupported rules
  950: check_rules('expire-at', _binary, notify) -> ok; %% for testing unsupported conditions
  951: check_rules(broken, rule, spec) -> ok;           %% for testing unacceptable rules
  952: check_rules(also_broken, rule, spec) -> ok;      %% for testing unacceptable rules
  953: 
  954: check_rules(C, V, A) -> throw({illegal_amp_rule, {C, V, A}}).
  955: 
  956: a2b(B) when is_binary(B) -> B;
  957: a2b(A) -> atom_to_binary(A, utf8).
  958: 
  959: %% Undefined-condition errors return a fully-fledged amp element with status=error
  960: %% The other errors have 'thin' <amp>s with no status attribute
  961: amp_status_attr(<<"undefined-condition">>) -> <<"error">>;
  962: amp_status_attr(_)                         -> no_status_attr.
  963: 
  964: amp_error_code(<<"undefined-condition">>) -> <<"500">>;
  965: amp_error_code(<<"not-acceptable">>) -> <<"405">>;
  966: amp_error_code(<<"unsupported-actions">>) -> <<"400">>;
  967: amp_error_code(<<"unsupported-conditions">>) -> <<"400">>.
  968: 
  969: amp_error_marker(<<"not-acceptable">>) -> <<"not-acceptable">>;
  970: amp_error_marker(<<"unsupported-actions">>) -> <<"bad-request">>;
  971: amp_error_marker(<<"unsupported-conditions">>) -> <<"bad-request">>;
  972: amp_error_marker(<<"undefined-condition">>) -> <<"undefined-condition">>.
  973: 
  974: amp_error_container(<<"not-acceptable">>) -> <<"invalid-rules">>;
  975: amp_error_container(<<"unsupported-actions">>) -> <<"unsupported-actions">>;
  976: amp_error_container(<<"unsupported-conditions">>) -> <<"unsupported-conditions">>;
  977: amp_error_container(<<"undefined-condition">>) -> <<"failed-rules">>.
  978: 
  979: is_module_loaded(Mod) ->
  980:     rpc(mim(), gen_mod, is_loaded, [host_type(), Mod]).
  981: 
  982: required_modules(basic) ->
  983:     mam_modules(off) ++ offline_modules(off) ++ privacy_modules(on);
  984: required_modules(mam) ->
  985:     mam_modules(on) ++ offline_modules(off) ++ privacy_modules(off);
  986: required_modules(offline) ->
  987:     mam_modules(off) ++ offline_modules(on) ++ privacy_modules(on);
  988: required_modules(_) ->
  989:     [].
  990: 
  991: mam_modules(on) ->
  992:     [{mod_mam, mam_helper:config_opts(#{pm => #{}, async_writer => #{enabled => false}})}];
  993: mam_modules(off) ->
  994:     [{mod_mam, stopped}].
  995: 
  996: offline_modules(on) ->
  997:     Backend = mongoose_helper:mnesia_or_rdbms_backend(),
  998:     [{mod_offline, config_parser_helper:mod_config(mod_offline,
  999:         #{backend => Backend,
 1000:           access_max_user_messages => max_user_offline_messages})}];
 1001: offline_modules(off) ->
 1002:     [{mod_offline, stopped},
 1003:      {mod_offline_stub, []}].
 1004: 
 1005: privacy_modules(on) ->
 1006:     Backend = mongoose_helper:mnesia_or_rdbms_backend(),
 1007:     [{mod_privacy, config_parser_helper:mod_config(mod_privacy, #{backend => Backend})},
 1008:      {mod_blocking, config_parser_helper:mod_config(mod_blocking, #{backend => Backend})}];
 1009: privacy_modules(off) ->
 1010:     [{mod_privacy, stopped},
 1011:      {mod_blocking, stopped}].
 1012: 
 1013: mod_offline_backend_module() ->
 1014:     Backend = mongoose_helper:mnesia_or_rdbms_backend(),
 1015:     case Backend of
 1016:         mnesia ->
 1017:             mod_offline_mnesia;
 1018:         rdbms ->
 1019:             mod_offline_rdbms
 1020:     end.