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(ejabberd_socket, send, fun ejabberd_socket_send/2).\n"
  128:     "ejabberd_socket_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, [ejabberd_socket, [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, {error, simulated}]);
  156: setup_meck(offline_failure) ->
  157:     ok = rpc(mim(), meck, expect, [mod_offline_mnesia, 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_mnesia]);
  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 ejabberd_socket mock
  344:               %% (see amp_test_helper_code/0)
  345:               Msg = amp_message_to(Bob, Rules, <<"Recipient connection breaks">>),
  346: 
  347:               %% when
  348:               client_sends_message(Alice, Msg),
  349: 
  350:               % then
  351:               case lists:member(Rule, Rules) of
  352:                   true -> client_receives_notification(Alice, Bob, Rule);
  353:                   false -> ok
  354:               end,
  355:               client_receives_nothing(Alice),
  356: 
  357:               %% Kill Bob's connection to avoid errors with closing the stream
  358:               %% while the session is being resumed after the simulated error
  359:               escalus_connection:kill(Bob)
  360:       end),
  361:     ok.
  362: 
  363: notify_deliver_to_offline_user_test(Config) ->
  364:     FreshConfig = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  365:     escalus:story(
  366:       FreshConfig, [{alice, 1}],
  367:       fun(Alice) ->
  368:               %% given
  369:               Rule = {deliver, case is_offline_storage_working(Config) of
  370:                                    true -> stored;
  371:                                    false -> none
  372:                                end, notify},
  373:               Rules = rules(Config, [Rule]),
  374:               BobJid = escalus_users:get_jid(FreshConfig, bob),
  375:               Msg = amp_message_to(BobJid, Rules, <<"A message in a bottle...">>),
  376: 
  377:               %% when
  378:               client_sends_message(Alice, Msg),
  379: 
  380:               % then
  381:               case lists:member(Rule, Rules) of
  382:                   true -> client_receives_notification(Alice, BobJid, Rule);
  383:                   false -> ok
  384:               end,
  385:               case ?config(offline_storage, Config) of
  386:                   offline_failure -> client_receives_generic_error(Alice, <<"500">>, <<"wait">>);
  387:                   _ -> client_receives_nothing(Alice)
  388:               end
  389:       end),
  390:     wait_until_no_session(FreshConfig, alice),
  391:     case is_offline_storage_working(Config) of
  392:         true -> user_has_incoming_offline_message(FreshConfig, bob, <<"A message in a bottle...">>);
  393:         false -> user_has_no_incoming_offline_messages(FreshConfig, bob)
  394:     end.
  395: 
  396: is_offline_storage_working(Config) ->
  397:     Status = ?config(offline_storage, Config),
  398:     Status == mam orelse Status == offline.
  399: 
  400: notify_deliver_to_offline_user_recipient_privacy_test(Config) ->
  401:     case is_module_loaded(mod_mam_pm) of
  402:         true -> {skip, "MAM does not support privacy lists"};
  403:         false -> do_notify_deliver_to_offline_user_recipient_privacy_test(Config)
  404:     end.
  405: 
  406: do_notify_deliver_to_offline_user_recipient_privacy_test(Config) ->
  407:     FreshConfig = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  408:     escalus:story(
  409:       FreshConfig, [{alice, 1}, {bob, 1}],
  410:       fun(Alice, Bob) ->
  411: 
  412:               privacy_helper:set_and_activate(Bob, <<"deny_all_message">>),
  413:               privacy_helper:set_default_list(Bob, <<"deny_all_message">>),
  414:               mongoose_helper:logout_user(Config, Bob),
  415:               %% given
  416:               Rule = {deliver, none, notify},
  417:               Rules = rules(Config, [Rule]),
  418:               BobJid = lbin(escalus_client:short_jid(Bob)),
  419: 
  420:               Msg = amp_message_to(BobJid, Rules, <<"A message in a bottle...">>),
  421: 
  422:               %% when
  423:               client_sends_message(Alice, Msg),
  424: 
  425:               % then
  426:               case lists:member(Rule, Rules) of
  427:                   true -> client_receives_notification(Alice, BobJid, Rule);
  428:                   false -> ok
  429:               end,
  430:               client_receives_nothing(Alice)
  431:       end),
  432:     user_has_no_incoming_offline_messages(FreshConfig, bob).
  433: 
  434: notify_deliver_to_stranger_test(Config) ->
  435:     escalus:fresh_story(
  436:       Config, [{alice, 1}],
  437:       fun(Alice) ->
  438:               %% given
  439:               Rule = {deliver, none, notify},
  440:               Rules = rules(Config, [Rule]),
  441:               Domain = domain(),
  442:               StrangerJid = <<"stranger@", Domain/binary>>,
  443:               Msg = amp_message_to(StrangerJid, Rules, <<"A message in a bottle...">>),
  444: 
  445:               %% when
  446:               client_sends_message(Alice, Msg),
  447: 
  448:               % then
  449:               case lists:member(Rule, Rules) of
  450:                   true -> client_receives_notification(Alice, StrangerJid, Rule);
  451:                   false -> ok
  452:               end,
  453:               client_receives_generic_error(Alice, <<"503">>, <<"cancel">>),
  454:               client_receives_nothing(Alice)
  455:       end).
  456: 
  457: notify_deliver_to_unknown_domain_test(Config) ->
  458:     escalus:fresh_story(
  459:       Config, [{alice, 1}],
  460:       fun(Alice) ->
  461:               %% given
  462:               StrangerJid = <<"stranger@unknown.domain">>,
  463:               Rule = {deliver, none, notify},
  464:               Rules = rules(Config, [Rule]),
  465:               Msg = amp_message_to(StrangerJid, Rules, <<"Msg to unknown domain">>),
  466:               %% when
  467:               client_sends_message(Alice, Msg),
  468: 
  469:               % then
  470:               case lists:member(Rule, Rules) of
  471:                   true -> client_receives_notification(Alice, StrangerJid, Rule);
  472:                   false -> ok
  473:               end,
  474:               % error 404: 'remote server not found' is expected
  475:               client_receives_generic_error(Alice, <<"404">>, <<"cancel">>),
  476:               client_receives_nothing(Alice)
  477:       end).
  478: 
  479: notify_match_resource_any_test(Config) ->
  480:     escalus:fresh_story(
  481:       Config, [{alice, 1}, {bob, 4}],
  482:       fun(Alice, Bob, _, _, _) ->
  483:               %% given
  484:               Msg = amp_message_to(Bob, [{'match-resource', any, notify}],
  485:                                    <<"Church-encoded hot-dogs">>),
  486:               %% when
  487:               client_sends_message(Alice, Msg),
  488: 
  489:               % then
  490:               client_receives_notification(Alice, Bob, {'match-resource', any, notify}),
  491:               client_receives_message(Bob, <<"Church-encoded hot-dogs">>)
  492:       end).
  493: 
  494: notify_match_resource_exact_test(Config) ->
  495:     escalus:fresh_story(
  496:       Config, [{alice, 1}, {bob, 4}],
  497:       fun(Alice, _, _, Bob3, _) ->
  498:               %% given
  499:               Msg = amp_message_to(Bob3, [{'match-resource', exact, notify}],
  500:                                    <<"Resource three, your battery is on fire!">>),
  501:               %% when
  502:               client_sends_message(Alice, Msg),
  503: 
  504:               % then
  505:               client_receives_notification(Alice, Bob3, {'match-resource', exact, notify}),
  506:               client_receives_message(Bob3, <<"Resource three, your battery is on fire!">>)
  507:       end).
  508: 
  509: notify_match_resource_other_test(Config) ->
  510:     escalus:fresh_story(
  511:       Config, [{alice, 1}, {bob, 1}],
  512:       fun(Alice, Bob) ->
  513:               %% given
  514:               NonmatchingJid = << (escalus_client:short_jid(Bob))/binary,
  515:                                   "/blahblahblah_resource" >>,
  516:               Msg = amp_message_to(NonmatchingJid,
  517:                                    [{'match-resource', other, notify}],
  518:                                     <<"A Bob by any other name!">>),
  519: 
  520:               %% when
  521:               client_sends_message(Alice, Msg),
  522: 
  523:               % then
  524:               client_receives_notification(Alice, NonmatchingJid,
  525:                                            {'match-resource', other, notify}),
  526:               client_receives_message(Bob, <<"A Bob by any other name!">>)
  527:       end).
  528: 
  529: notify_match_resource_other_bare_test(Config) ->
  530:     escalus:fresh_story(
  531:       Config, [{alice, 1}, {bob, 1}],
  532:       fun(Alice, Bob) ->
  533:               %% given
  534:               BareJid = escalus_client:short_jid(Bob),
  535:               Msg = amp_message_to(BareJid,
  536:                                    [{'match-resource', other, notify}],
  537:                                     <<"A Bob by any other name!">>),
  538: 
  539:               %% when
  540:               client_sends_message(Alice, Msg),
  541: 
  542:               % then
  543:               client_receives_notification(Alice, BareJid, {'match-resource', other, notify}),
  544:               client_receives_message(Bob, <<"A Bob by any other name!">>)
  545:       end).
  546: 
  547: error_deliver_to_online_user_test(Config) ->
  548:     escalus:fresh_story(
  549:       Config, [{alice, 1}, {bob, 1}],
  550:       fun(Alice, Bob) ->
  551:               %% given
  552:               Rule = {deliver, direct, error},
  553:               Rules = rules(Config, [Rule]),
  554:               Msg = amp_message_to(Bob, Rules, <<"It might cause an error">>),
  555: 
  556:               %% when
  557:               client_sends_message(Alice, Msg),
  558: 
  559:               % then
  560:               case lists:member(Rule, Rules) of
  561:                   true ->
  562:                       client_receives_amp_error(Alice, Bob, Rule, <<"undefined-condition">>),
  563:                       client_receives_nothing(Bob);
  564:                   false ->
  565:                       client_receives_message(Bob, <<"It might cause an error">>)
  566:               end,
  567:               client_receives_nothing(Alice)
  568:       end).
  569: 
  570: error_deliver_to_offline_user_test(Config) ->
  571:     FreshConfig = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  572:     Rule = {deliver, case ?config(offline_storage, Config) of
  573:                          none -> none;
  574:                          _ -> stored
  575:                      end, error},
  576:     Rules = rules(Config, [Rule]),
  577:     escalus:story(
  578:       FreshConfig, [{alice, 1}],
  579:       fun(Alice) ->
  580:               %% given
  581:               BobJid = escalus_users:get_jid(FreshConfig, bob),
  582:               Msg = amp_message_to(BobJid, Rules, <<"A message in a bottle...">>),
  583: 
  584:               %% when
  585:               client_sends_message(Alice, Msg),
  586: 
  587:               % then
  588:               case lists:member(Rule, Rules) of
  589:                   true ->
  590:                       client_receives_amp_error(Alice, BobJid, Rule, <<"undefined-condition">>);
  591:                   false ->
  592:                       check_offline_storage(Alice, Config)
  593:               end
  594:       end),
  595:     wait_until_no_session(FreshConfig, alice),
  596:     case is_offline_storage_working(Config) andalso not lists:member(Rule, Rules) of
  597:         true -> user_has_incoming_offline_message(FreshConfig, bob, <<"A message in a bottle...">>);
  598:         false -> user_has_no_incoming_offline_messages(FreshConfig, bob)
  599:     end.
  600: 
  601: check_offline_storage(User, Config) ->
  602:     case ?config(offline_storage, Config) of
  603:         offline_failure ->
  604:             client_receives_generic_error(User, <<"500">>, <<"wait">>);
  605:         _ -> client_receives_nothing(User)
  606:     end.
  607: 
  608: error_deliver_to_stranger_test(Config) ->
  609:     escalus:fresh_story(
  610:       Config, [{alice, 1}],
  611:       fun(Alice) ->
  612:               %% given
  613:               Rule = {deliver, none, error},
  614:               Rules = rules(Config, [Rule]),
  615:               Domain = domain(),
  616:               StrangerJid = <<"stranger@", Domain/binary>>,
  617:               Msg = amp_message_to(StrangerJid, Rules, <<"This cannot possibly succeed">>),
  618: 
  619:               %% when
  620:               client_sends_message(Alice, Msg),
  621: 
  622:               % then
  623:               case lists:member(Rule, Rules) of
  624:                   true -> client_receives_amp_error(Alice, StrangerJid, Rule,
  625:                                                     <<"undefined-condition">>);
  626:                   false -> client_receives_generic_error(Alice, <<"503">>, <<"cancel">>)
  627:               end,
  628:               client_receives_nothing(Alice)
  629: 
  630:       end).
  631: 
  632: drop_deliver_to_online_user_test(Config) ->
  633:     escalus:fresh_story(
  634:       Config, [{alice, 1}, {bob, 1}],
  635:       fun(Alice, Bob) ->
  636:               %% given
  637:               Rule = {deliver, direct, drop},
  638:               Rules = rules(Config, [Rule]),
  639:               Msg = amp_message_to(Bob, Rules, <<"It might get dropped">>),
  640: 
  641:               %% when
  642:               client_sends_message(Alice, Msg),
  643: 
  644:               % then
  645:               case lists:member(Rule, Rules) of
  646:                   true -> client_receives_nothing(Bob);
  647:                   false -> client_receives_message(Bob, <<"It might get dropped">>)
  648:               end,
  649:               client_receives_nothing(Alice)
  650:       end).
  651: 
  652: drop_deliver_to_offline_user_test(Config) ->
  653:     FreshConfig = escalus_fresh:create_users(Config, [{alice, 1}, {bob, 1}]),
  654:     Rule = {deliver, case ?config(offline_storage, Config) of
  655:                          none -> none;
  656:                          _ -> stored
  657:                      end, drop},
  658:     Rules = rules(Config, [Rule]),
  659:     Message = <<"A message in a bottle...">>,
  660:     escalus:story(
  661:       FreshConfig, [{alice, 1}],
  662:       fun(Alice) ->
  663:               %% given
  664:               BobJid = escalus_users:get_jid(FreshConfig, bob),
  665:               Msg = amp_message_to(BobJid, Rules, Message),
  666: 
  667:               %% when
  668:               client_sends_message(Alice, Msg),
  669: 
  670:               % then
  671:               case lists:member(Rule, Rules) orelse
  672:                    ?config(offline_storage, Config) /= offline_failure of
  673:                   true -> client_receives_nothing(Alice);
  674:                   false -> client_receives_generic_error(Alice, <<"500">>, <<"wait">>)
  675:               end
  676:       end),
  677:     wait_until_no_session(FreshConfig, alice),
  678:     case is_offline_storage_working(Config) andalso not lists:member(Rule, Rules) of
  679:         true -> user_has_incoming_offline_message(FreshConfig, bob, Message);
  680:         false -> user_has_no_incoming_offline_messages(FreshConfig, bob)
  681:     end.
  682: 
  683: drop_deliver_to_stranger_test(Config) ->
  684:     escalus:fresh_story(
  685:       Config, [{alice, 1}],
  686:       fun(Alice) ->
  687:               %% given
  688:               Rule = {deliver, none, drop},
  689:               Rules = rules(Config, [Rule]),
  690:               Domain = domain(),
  691:               StrangerJid = <<"stranger@", Domain/binary>>,
  692:               Msg = amp_message_to(StrangerJid, Rules, <<"This cannot possibly succeed">>),
  693: 
  694:               %% when
  695:               client_sends_message(Alice, Msg),
  696: 
  697:               % then
  698:               case lists:member(Rule, Rules) of
  699:                   true -> ok;
  700:                   false -> client_receives_generic_error(Alice, <<"503">>, <<"cancel">>)
  701:               end,
  702:               client_receives_nothing(Alice)
  703: 
  704:       end).
  705: 
  706: last_rule_applies_test(Config) ->
  707:     escalus:fresh_story(
  708:       Config, [{alice, 1}, {bob, 1}],
  709:       fun(Alice, Bob) ->
  710:               %% given
  711:               BobsBareJid = escalus_client:short_jid(Bob),
  712:               Msg = amp_message_to(BobsBareJid, [{deliver, none, error},
  713:                                                  {deliver, stored, error},
  714:                                                  {deliver, direct, notify}],
  715:                                    <<"One of your resources needs to get this!">>),
  716:               %% when
  717:               client_sends_message(Alice, Msg),
  718: 
  719:               % then
  720:               client_receives_notification(Alice, BobsBareJid, {deliver, direct, notify}),
  721:               client_receives_message(Bob, <<"One of your resources needs to get this!">>)
  722:       end).
  723: 
  724: %% Internal
  725: 
  726: wait_until_no_session(FreshConfig, User) ->
  727:     U = escalus_users:get_username(FreshConfig, User),
  728:     S = escalus_users:get_server(FreshConfig, User),
  729:     JID = jid:make(U, S, <<>>),
  730:     mongoose_helper:wait_until(
  731:       fun() -> rpc(mim(), ejabberd_sm, get_user_resources, [JID]) end, []).
  732: 
  733: user_has_no_incoming_offline_messages(FreshConfig, UserName) ->
  734:     escalus:fresh_story(
  735:       FreshConfig, [{UserName, 1}],
  736:       fun(User) ->
  737:               client_receives_nothing(User),
  738:               case is_module_loaded(mod_mam_pm) of
  739:                   true -> client_has_no_mam_messages(User);
  740:                   false -> ok
  741:               end
  742:       end).
  743: 
  744: user_has_incoming_offline_message(FreshConfig, UserName, MsgText) ->
  745:     true = is_module_loaded(mod_mam_pm) orelse is_module_loaded(mod_offline),
  746:     {ok, Client} = escalus_client:start(FreshConfig, UserName, <<"new-session">>),
  747:     escalus:send(Client, escalus_stanza:presence(<<"available">>)),
  748:     case is_module_loaded(mod_offline) of
  749:         true -> client_receives_message(Client, MsgText);
  750:         false -> ok
  751:     end,
  752:     Presence = escalus:wait_for_stanza(Client),
  753:     escalus:assert(is_presence, Presence),
  754:     case is_module_loaded(mod_mam_pm) of
  755:         true -> client_has_mam_message(Client);
  756:         false -> ok
  757:     end,
  758:     escalus_client:stop(FreshConfig, Client).
  759: 
  760: client_has_no_mam_messages(User) ->
  761:     P = mam_helper:mam04_props(),
  762:     escalus:send(User, mam_helper:stanza_archive_request(P, <<"q1">>)),
  763:     Res = mam_helper:wait_archive_respond(User),
  764:     mam_helper:assert_respond_size(0, Res).
  765: 
  766: client_has_mam_message(User) ->
  767:     P = mam_helper:mam04_props(),
  768:     escalus:send(User, mam_helper:stanza_archive_request(P, <<"q1">>)),
  769:     Res = mam_helper:wait_archive_respond(User),
  770:     mam_helper:assert_respond_size(1, Res).
  771: 
  772: rules(Config, Default) ->
  773:     case lists:keysearch(rules, 1, Config) of
  774:         {value, {rules, Val}} -> Val;
  775:         _ -> Default
  776:     end.
  777: 
  778: ns_amp() ->
  779:     <<"http://jabber.org/protocol/amp">>.
  780: 
  781: client_sends_message(Client, Msg) ->
  782:     escalus_client:send(Client, Msg).
  783: 
  784: client_receives_amp_error(Client, Rules, AmpErrorKind) when is_list(Rules) ->
  785:     Received = escalus_client:wait_for_stanza(Client),
  786:     assert_amp_error(Client, Received, Rules, AmpErrorKind);
  787: client_receives_amp_error(Client, Rule, AmpErrorKind) ->
  788:     client_receives_amp_error(Client, [Rule], AmpErrorKind).
  789: 
  790: client_receives_amp_error(Client, IntendedRecipient, Rule, AmpErrorKind) ->
  791:     Received = escalus_client:wait_for_stanza(Client),
  792:     assert_amp_error_with_full_amp(Client, IntendedRecipient,
  793:                                    Received, Rule, AmpErrorKind).
  794: 
  795: client_receives_generic_error(Client, Code, Type) ->
  796:     Received = escalus_client:wait_for_stanza(Client, 5000),
  797:     escalus:assert(fun contains_error/3, [Code, Type], Received).
  798: 
  799: client_receives_nothing(Client) ->
  800:     timer:sleep(300),
  801:     escalus_assert:has_no_stanzas(Client).
  802: 
  803: client_receives_message(Client, MsgText) ->
  804:     Received = escalus_client:wait_for_stanza(Client),
  805:     escalus:assert(is_chat_message, [MsgText], Received).
  806: 
  807: client_receives_notification(Client, IntendedRecipient, Rule) ->
  808:     Msg = escalus_client:wait_for_stanza(Client),
  809:     assert_notification(Client, IntendedRecipient, Msg, Rule).
  810: 
  811: disco_info() ->
  812:     escalus_stanza:disco_info(domain()).
  813: 
  814: disco_info_amp_node() ->
  815:     escalus_stanza:disco_info(domain(), ns_amp()).
  816: 
  817: assert_amp_error(Client, Response, Rules, AmpErrorKind) when is_list(Rules) ->
  818:     ClientJID = escalus_client:full_jid(Client),
  819:     Server = escalus_client:server(Client),
  820:     Server = exml_query:attr(Response, <<"from">>),
  821:     ClientJID = exml_query:attr(Response, <<"to">>),
  822:     escalus:assert(fun contains_amp/5,
  823:                    [amp_status_attr(AmpErrorKind), no_to_attr, no_from_attr, Rules],
  824:                    Response),
  825:     escalus:assert(fun contains_amp_error/3,
  826:                    [AmpErrorKind, Rules],
  827:                    Response);
  828: 
  829: assert_amp_error(Client, Response, Rule, AmpErrorKind) ->
  830:     assert_amp_error(Client, Response, [Rule], AmpErrorKind).
  831: 
  832: assert_amp_error_with_full_amp(Client, IntendedRecipient, Response,
  833:                                {_C, _V, _A} = Rule, AmpErrorKind) ->
  834:     ClientJID = escalus_client:full_jid(Client),
  835:     RecipientJID = full_jid(IntendedRecipient),
  836:     Server = escalus_client:server(Client),
  837:     Server = exml_query:attr(Response, <<"from">>),
  838:     ClientJID = exml_query:attr(Response, <<"to">>),
  839:     escalus:assert(fun contains_amp/5,
  840:                    [amp_status_attr(AmpErrorKind), RecipientJID, ClientJID, [Rule]],
  841:                    Response),
  842:     escalus:assert(fun contains_amp_error/3,
  843:                    [AmpErrorKind, [Rule]],
  844:                    Response).
  845: 
  846: 
  847: assert_notification(Client, IntendedRecipient, Response, {_C, _V, A} = Rule) ->
  848:     ClientJID = escalus_client:full_jid(Client),
  849:     RecipientJID = full_jid(IntendedRecipient),
  850:     Server = escalus_client:server(Client),
  851:     Action = a2b(A),
  852:     Server = exml_query:attr(Response, <<"from">>),
  853:     ClientJID = exml_query:attr(Response, <<"to">>),
  854:     escalus:assert(fun contains_amp/5,
  855:                    [Action, RecipientJID, ClientJID, [Rule]],
  856:                    Response).
  857: 
  858: assert_has_features(Response, Features) ->
  859:     CheckF = fun(F) -> escalus:assert(has_feature, [F], Response) end,
  860:     lists:foreach(CheckF, Features).
  861: 
  862: full_jid(#client{} = Client) ->
  863:     escalus_client:full_jid(Client);
  864: full_jid(B) when is_binary(B) ->
  865:     B.
  866: 
  867: %% @TODO: Move me out to escalus_stanza %%%%%%%%%
  868: %%%%%%%%% Element constructors %%%%%%%%%%%%%%%%%%
  869: amp_message_to(To, Rules, MsgText) ->
  870:     Msg0 = #xmlel{children=C} = escalus_stanza:chat_to(To, MsgText),
  871:     Msg = escalus_stanza:set_id(Msg0, escalus_stanza:id()),
  872:     Amp = amp_el(Rules),
  873:     Msg#xmlel{children = C ++ [Amp]}.
  874: 
  875: amp_el([]) ->
  876:     throw("cannot build <amp> with no rules!");
  877: amp_el(Rules) ->
  878:     #xmlel{name = <<"amp">>,
  879:            attrs = [{<<"xmlns">>, ns_amp()}],
  880:            children = [ rule_el(R) || R <- Rules ]}.
  881: 
  882: rule_el({Condition, Value, Action}) ->
  883:     check_rules(Condition, Value, Action),
  884:     #xmlel{name = <<"rule">>
  885:           , attrs = [{<<"condition">>, a2b(Condition)}
  886:                     , {<<"value">>, a2b(Value)}
  887:                     , {<<"action">>, a2b(Action)}]}.
  888: 
  889: %% @TODO: Move me out to escalus_pred %%%%%%%%%%%%
  890: %%%%%%%%% XML predicates %%%%% %%%%%%%%%%%%%%%%%%%
  891: contains_amp(Status, To, From, ExpectedRules, Stanza) when is_list(ExpectedRules)->
  892:     Amp = exml_query:subelement(Stanza, <<"amp">>),
  893:     undefined =/= Amp andalso
  894:         To == exml_query:attr(Amp, <<"to">>, no_to_attr) andalso
  895:         From == exml_query:attr(Amp, <<"from">>, no_from_attr) andalso
  896:         Status == exml_query:attr(Amp, <<"status">>, no_status_attr) andalso
  897:         all_present([ rule_el(R) || R <- ExpectedRules ], exml_query:subelements(Amp, <<"rule">>)).
  898: 
  899: contains_amp_error(AmpErrorKind, Rules, Response) ->
  900:     ErrorEl = exml_query:subelement(Response, <<"error">>),
  901:     <<"modify">> == exml_query:attr(ErrorEl, <<"type">>)
  902:         andalso
  903:         amp_error_code(AmpErrorKind) == exml_query:attr(ErrorEl, <<"code">>)
  904:         andalso
  905:         undefined =/= (Marker = exml_query:subelement(ErrorEl, amp_error_marker(AmpErrorKind)))
  906:         andalso
  907:         ns_stanzas() == exml_query:attr(Marker, <<"xmlns">>)
  908:         andalso
  909:         undefined =/= (Container = exml_query:subelement(ErrorEl,
  910:                                             amp_error_container(AmpErrorKind)))
  911:         andalso
  912:         all_present([ rule_el(R) || R <- Rules ], exml_query:subelements(Container, <<"rule">>)).
  913: 
  914: contains_error(Code, Type, Response) ->
  915:     ErrorEl = exml_query:subelement(Response, <<"error">>),
  916:     Type == exml_query:attr(ErrorEl, <<"type">>)
  917:         andalso (Code == any orelse Code == exml_query:attr(ErrorEl, <<"code">>)).
  918: 
  919: all_present(Needles, Haystack) ->
  920:     list_and([ lists:member(Needle, Haystack)
  921:                || Needle <- Needles ]).
  922: 
  923: 
  924: list_and(List) ->
  925:     lists:all(fun(X) -> X =:= true end, List).
  926: 
  927: ns_stanzas() ->
  928:     <<"urn:ietf:params:xml:ns:xmpp-stanzas">>.
  929: 
  930: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  931: 
  932: check_rules(deliver, direct, notify) -> ok;
  933: check_rules(deliver, stored, notify) -> ok;
  934: check_rules(deliver, none, notify) -> ok;
  935: 
  936: check_rules(deliver, direct, error) -> ok;
  937: check_rules(deliver, stored, error) -> ok;
  938: check_rules(deliver, none, error) -> ok;
  939: 
  940: check_rules(deliver, direct, drop) -> ok;
  941: check_rules(deliver, stored, drop) -> ok;
  942: check_rules(deliver, none, drop) -> ok;
  943: 
  944: check_rules('match-resource', any, notify) -> ok;
  945: check_rules('match-resource', exact, notify) -> ok;
  946: check_rules('match-resource', other, notify) -> ok;
  947: 
  948: check_rules(deliver, direct, alert) -> ok;       %% for testing unsupported rules
  949: check_rules('expire-at', _binary, notify) -> ok; %% for testing unsupported conditions
  950: check_rules(broken, rule, spec) -> ok;           %% for testing unacceptable rules
  951: check_rules(also_broken, rule, spec) -> ok;      %% for testing unacceptable rules
  952: 
  953: check_rules(C, V, A) -> throw({illegal_amp_rule, {C, V, A}}).
  954: 
  955: a2b(B) when is_binary(B) -> B;
  956: a2b(A) -> atom_to_binary(A, utf8).
  957: 
  958: %% Undefined-condition errors return a fully-fledged amp element with status=error
  959: %% The other errors have 'thin' <amp>s with no status attribute
  960: amp_status_attr(<<"undefined-condition">>) -> <<"error">>;
  961: amp_status_attr(_)                         -> no_status_attr.
  962: 
  963: amp_error_code(<<"undefined-condition">>) -> <<"500">>;
  964: amp_error_code(<<"not-acceptable">>) -> <<"405">>;
  965: amp_error_code(<<"unsupported-actions">>) -> <<"400">>;
  966: amp_error_code(<<"unsupported-conditions">>) -> <<"400">>.
  967: 
  968: amp_error_marker(<<"not-acceptable">>) -> <<"not-acceptable">>;
  969: amp_error_marker(<<"unsupported-actions">>) -> <<"bad-request">>;
  970: amp_error_marker(<<"unsupported-conditions">>) -> <<"bad-request">>;
  971: amp_error_marker(<<"undefined-condition">>) -> <<"undefined-condition">>.
  972: 
  973: amp_error_container(<<"not-acceptable">>) -> <<"invalid-rules">>;
  974: amp_error_container(<<"unsupported-actions">>) -> <<"unsupported-actions">>;
  975: amp_error_container(<<"unsupported-conditions">>) -> <<"unsupported-conditions">>;
  976: amp_error_container(<<"undefined-condition">>) -> <<"failed-rules">>.
  977: 
  978: is_module_loaded(Mod) ->
  979:     rpc(mim(), gen_mod, is_loaded, [host_type(), Mod]).
  980: 
  981: required_modules(basic) ->
  982:     mam_modules(off) ++ offline_modules(off) ++ privacy_modules(on);
  983: required_modules(mam) ->
  984:     mam_modules(on) ++ offline_modules(off) ++ privacy_modules(off);
  985: required_modules(offline) ->
  986:     mam_modules(off) ++ offline_modules(on) ++ privacy_modules(on);
  987: required_modules(_) ->
  988:     [].
  989: 
  990: mam_modules(on) ->
  991:     [{mod_mam, mam_helper:config_opts(#{pm => #{}, async_writer => #{enabled => false}})}];
  992: mam_modules(off) ->
  993:     [{mod_mam, stopped}].
  994: 
  995: offline_modules(on) ->
  996:     [{mod_offline, config_parser_helper:mod_config(mod_offline,
  997:         #{access_max_user_messages => max_user_offline_messages})}];
  998: offline_modules(off) ->
  999:     [{mod_offline, stopped},
 1000:      {mod_offline_stub, []}].
 1001: 
 1002: privacy_modules(on) ->
 1003:     [{mod_privacy, config_parser_helper:default_mod_config(mod_privacy)},
 1004:      {mod_blocking, config_parser_helper:default_mod_config(mod_blocking)}];
 1005: privacy_modules(off) ->
 1006:     [{mod_privacy, stopped},
 1007:      {mod_blocking, stopped}].