1: -module(event_pusher_sns_SUITE).
    2: 
    3: -compile([export_all, nowarn_export_all]).
    4: 
    5: -include("mongoose.hrl").
    6: -include("mod_event_pusher_events.hrl").
    7: -include_lib("exml/include/exml.hrl").
    8: -include_lib("common_test/include/ct.hrl").
    9: -include_lib("eunit/include/eunit.hrl").
   10: 
   11: -define(ACC_PARAMS, #{location => ?LOCATION,
   12:                       host_type => host_type(),
   13:                       lserver => domain(),
   14:                       element => undefined}).
   15: 
   16: -import(config_parser_helper, [config/2, mod_config/2]).
   17: 
   18: all() ->
   19:     [
   20:      handles_unicode_messages,
   21:      forwards_chat_messages_to_chat_topic,
   22:      forwards_groupchat_messages_to_groupchat_topic,
   23:      does_not_forward_other_messages,
   24:      creates_proper_sns_topic_arn,
   25:      forwards_online_presence_to_presence_topic,
   26:      forwards_offline_presence_to_presence_topic,
   27:      does_not_forward_messages_without_body,
   28:      does_not_forward_messages_when_topic_is_unset,
   29:      does_not_forward_presences_when_topic_is_unset,
   30:      calls_callback_module_to_get_user_id,
   31:      calls_callback_module_to_retrieve_attributes_for_presence,
   32:      calls_callback_module_to_retrieve_attributes_for_message
   33:     ].
   34: 
   35: %% Tests
   36: 
   37: handles_unicode_messages(Config) ->
   38:     expect_message_entry(message, <<"❤☀☆☂☻♞☯☭☢€"/utf8>>),
   39:     send_packet_callback(Config, <<"chat">>, <<"❤☀☆☂☻♞☯☭☢€"/utf8>>).
   40: 
   41: forwards_chat_messages_to_chat_topic(Config) ->
   42:     expect_topic("user_message_sent-dev-1"),
   43:     send_packet_callback(Config, <<"chat">>, <<"message">>).
   44: 
   45: forwards_groupchat_messages_to_groupchat_topic(Config) ->
   46:     expect_topic("user_messagegroup_sent-dev-1"),
   47:     send_packet_callback(Config, <<"groupchat">>, <<"message">>).
   48: 
   49: does_not_forward_other_messages(Config) ->
   50:     meck:expect(erlcloud_sns, publish, fun(_, _, _, _, _, _) -> ok end),
   51:     send_packet_callback(Config, <<"othertype">>, <<"message">>),
   52:     ?assertNot(meck:called(erlcloud_sns, publish, '_')).
   53: 
   54: creates_proper_sns_topic_arn(Config) ->
   55:     meck:expect(erlcloud_sns, publish, fun(_, _, _, _, _, _) -> ok end),
   56:     ExpectedTopic = craft_arn("user_message_sent-dev-1"),
   57:     send_packet_callback(Config, <<"chat">>, <<"message">>),
   58:     ?assert(meck:called(erlcloud_sns, publish, [topic, ExpectedTopic, '_', '_', '_', '_'])).
   59: 
   60: forwards_online_presence_to_presence_topic(Config) ->
   61:     expect_message_entry(present, true),
   62:     user_present_callback(Config),
   63:     ExpectedTopic = craft_arn("user_presence_updated-dev-1"),
   64:     ?assert(meck:called(erlcloud_sns, publish, [topic, ExpectedTopic, '_', '_', '_', '_'])).
   65: 
   66: forwards_offline_presence_to_presence_topic(Config) ->
   67:     expect_message_entry(present, false),
   68:     user_not_present_callback(Config),
   69:     ExpectedTopic = craft_arn("user_presence_updated-dev-1"),
   70:     ?assert(meck:called(erlcloud_sns, publish, [topic, ExpectedTopic, '_', '_', '_', '_'])).
   71: 
   72: does_not_forward_messages_without_body(Config) ->
   73:     meck:expect(erlcloud_sns, publish, fun(_, _, _, _, _, _) -> ok end),
   74:     send_packet_callback(Config, <<"chat">>, undefined),
   75:     ?assertNot(meck:called(erlcloud_sns, publish, '_')).
   76: 
   77: does_not_forward_messages_when_topic_is_unset(Config) ->
   78:     meck:expect(erlcloud_sns, publish, fun(_, _, _, _, _, _) -> ok end),
   79:     send_packet_callback(Config, <<"chat">>, <<"message">>),
   80:     ?assertNot(meck:called(erlcloud_sns, publish, '_')).
   81: 
   82: does_not_forward_presences_when_topic_is_unset(Config) ->
   83:     meck:expect(erlcloud_sns, publish, fun(_, _, _, _, _, _) -> ok end),
   84:     user_not_present_callback(Config),
   85:     ?assertNot(meck:called(erlcloud_sns, publish, '_')).
   86: 
   87: calls_callback_module_to_get_user_id(Config) ->
   88:     Module = custom_callback_module("customuserid", #{}),
   89:     expect_message_entry(user_id, "customuserid"),
   90:     user_not_present_callback(Config),
   91:     ?assert(meck:called(Module, user_guid, '_')).
   92: 
   93: calls_callback_module_to_retrieve_attributes_for_presence(Config) ->
   94:     Module = custom_callback_module("", #{"a" => 123}),
   95:     expect_attributes("a", 123),
   96:     user_not_present_callback(Config),
   97:     ?assert(meck:called(Module, message_attributes, ['_', '_', '_'])).
   98: 
   99: calls_callback_module_to_retrieve_attributes_for_message(Config) ->
  100:     Module = custom_callback_module("", #{"b" => "abc"}),
  101:     expect_attributes("b", "abc"),
  102:     send_packet_callback(Config, <<"groupchat">>, <<"message">>),
  103:     ?assert(meck:called(Module, message_attributes, ['_', '_', '_', '_', '_'])).
  104: 
  105: %% Fixtures
  106: 
  107: init_per_suite(Config) ->
  108:     {ok, _} = application:ensure_all_started(jid),
  109:     Config.
  110: 
  111: end_per_suite(_) ->
  112:     ok.
  113: 
  114: init_per_testcase(CaseName, Config) ->
  115:     mongooseim_helper:start_link_loaded_hooks(),
  116:     meck:new(erlcloud_sns, [non_strict, passthrough]),
  117:     meck:new([mongoose_wpool, mongoose_metrics], [stub_all]),
  118:     meck:expect(erlcloud_sns, new, fun(_, _, _) -> mod_aws_sns_SUITE_erlcloud_sns_new end),
  119:     meck:expect(mongoose_wpool, start, fun(_, _, _, _) -> {ok, mocked} end),
  120:     meck:expect(mongoose_wpool, cast, fun(_, _, _, {M, F, A}) -> erlang:apply(M, F, A) end),
  121:     start_modules(sns_config(CaseName)),
  122:     [{sender, jid:from_binary(<<"sender@localhost">>)},
  123:      {recipient, jid:from_binary(<<"recipient@localhost">>)} |
  124:      Config].
  125: 
  126: end_per_testcase(_, _Config) ->
  127:     stop_modules(),
  128:     meck:unload().
  129: 
  130: %% Wrapped callbacks
  131: 
  132: send_packet_callback(Config, Type, Body) ->
  133:     Packet = message(Config, Type, Body),
  134:     Sender = ?config(sender, Config),
  135:     Recipient = ?config(recipient, Config),
  136:     mod_event_pusher_sns:push_event(mongoose_acc:new(?ACC_PARAMS),
  137:                                     #chat_event{type = chat, direction = in,
  138:                                                 from = Sender, to = Recipient,
  139:                                                 packet = Packet}).
  140: 
  141: user_present_callback(Config) ->
  142:     Jid = ?config(sender, Config),
  143:     mod_event_pusher_sns:push_event(mongoose_acc:new(?ACC_PARAMS),
  144:                                     #user_status_event{jid = Jid, status = online}).
  145: 
  146: user_not_present_callback(Config) ->
  147:     Jid = ?config(sender, Config),
  148:     mod_event_pusher_sns:push_event(mongoose_acc:new(?ACC_PARAMS),
  149:                                     #user_status_event{jid = Jid, status = offline}).
  150: 
  151: %% Helpers
  152: 
  153: custom_callback_module(UserId, Attributes) ->
  154:     Module = custom_callback_module(),
  155:     meck:new(Module, [non_strict]),
  156:     meck:expect(Module, user_guid, fun(_) -> UserId end),
  157:     meck:expect(Module, message_attributes, fun(_, _, _) -> Attributes end),
  158:     meck:expect(Module, message_attributes, fun(_, _, _, _, _) -> Attributes end),
  159:     Module.
  160: 
  161: custom_callback_module() ->
  162:     mod_aws_sns_SUITE_mockcb.
  163: 
  164: craft_arn(Topic) ->
  165:     #{region := Region, account_id := AccountId} = common_sns_opts(),
  166:     craft_arn(Region, AccountId, Topic).
  167: 
  168: craft_arn(Region, UserId, Topic) ->
  169:     "arn:aws:sns:" ++ Region ++ ":" ++ UserId ++ ":" ++ Topic.
  170: 
  171: expect_topic(ExpectedTopic) ->
  172:     meck:expect(erlcloud_sns, publish,
  173:                 fun(topic, Topic, _, _, _, _) -> true = lists:suffix(ExpectedTopic, Topic) end).
  174: 
  175: expect_message_entry(Key, Value) ->
  176:     meck:expect(
  177:       erlcloud_sns, publish,
  178:       fun(_, _, TupleList, _, _, _) ->
  179:               MessageObject = maps:from_list(TupleList),
  180:               Value = maps:get(Key, MessageObject)
  181:       end).
  182: 
  183: expect_attributes(Key, Value) ->
  184:     meck:expect(
  185:       erlcloud_sns, publish,
  186:       fun(_, _, _, _, Attrs, _) -> Value = proplists:get_value(Key, Attrs) end).
  187: 
  188: sns_config(does_not_forward_messages_when_topic_is_unset) ->
  189:     maps:remove(pm_messages_topic, common_sns_opts());
  190: sns_config(does_not_forward_presences_when_topic_is_unset) ->
  191:     maps:remove(presence_updates_topic, common_sns_opts());
  192: sns_config(CN) when CN =:= calls_callback_module_to_get_user_id;
  193:                     CN =:= calls_callback_module_to_retrieve_attributes_for_presence;
  194:                     CN =:= calls_callback_module_to_retrieve_attributes_for_message ->
  195:     (common_sns_opts())#{plugin_module => custom_callback_module()};
  196: sns_config(_) ->
  197:     common_sns_opts().
  198: 
  199: start_modules(SNSExtra) ->
  200:     mongoose_config:set_opts(opts(SNSExtra)),
  201:     mongoose_modules:start().
  202: 
  203: stop_modules() ->
  204:     mongoose_modules:stop(),
  205:     mongoose_config:erase_opts().
  206: 
  207: opts(SNSExtra) ->
  208:     #{hosts => [host_type()],
  209:       host_types => [],
  210:       all_metrics_are_global => false,
  211:       {modules, host_type()} => modules(SNSExtra)}.
  212: 
  213: modules(SNSExtra) ->
  214:     gen_mod_deps:resolve_deps(host_type(), #{mod_event_pusher => module_opts(SNSExtra)}).
  215: 
  216: module_opts(SNSExtra) ->
  217:     SNSOpts = config([modules, mod_event_pusher, sns], SNSExtra),
  218:     mod_config(mod_event_pusher, #{sns => SNSOpts}).
  219: 
  220: common_sns_opts() ->
  221:     #{sns_host => "sns.eu-west-1.amazonaws.com",
  222:       region => "eu-west-1",
  223:       access_key_id => "AKIAJAZYHOIPY6A2PESA",
  224:       secret_access_key => "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",
  225:       account_id => "251423380551",
  226:       presence_updates_topic => "user_presence_updated-dev-1",
  227:       pm_messages_topic => "user_message_sent-dev-1",
  228:       muc_messages_topic => "user_messagegroup_sent-dev-1"}.
  229: 
  230: message(Config, Type, Body) ->
  231:     message(?config(sender, Config), ?config(recipient, Config), Type, Body).
  232: 
  233: message(From, Recipient, Type, Body) ->
  234:     Children =
  235:         case Body of
  236:             undefined -> [];
  237:             _ -> [#xmlel{name = <<"body">>, children = [#xmlcdata{content = Body}]}]
  238:         end,
  239:     #xmlel{name = <<"message">>,
  240:            attrs = [{<<"from">>, jid:to_binary(From)}, {<<"type">>, Type},
  241:                     {<<"to">>, jid:to_binary(Recipient)}],
  242:            children = Children}.
  243: 
  244: host_type() ->
  245:     domain().
  246: 
  247: domain() ->
  248:     <<"localhost">>.