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