1: -module(push_http_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("exml/include/exml.hrl").
    5: -include_lib("escalus/include/escalus.hrl").
    6: -include_lib("common_test/include/ct.hrl").
    7: -include_lib("eunit/include/eunit.hrl").
    8: -include_lib("escalus/include/escalus_xmlns.hrl").
    9: 
   10: -define(ETS_TABLE, push_http).
   11: 
   12: -import(push_helper, [http_notifications_port/0, http_notifications_host/0]).
   13: 
   14: -import(domain_helper, [domain/0]).
   15: 
   16: %%--------------------------------------------------------------------
   17: %% Suite configuration
   18: %%--------------------------------------------------------------------
   19: 
   20: all() ->
   21:     [
   22:         {group, single},
   23:         {group, customised},
   24:         {group, multiple}
   25:     ].
   26: 
   27: groups() ->
   28:     [{single, [sequence], [simple_push]},
   29:      {customised, [sequence], [custom_push]},
   30:      {multiple, [sequence], [push_to_many]}
   31:     ].
   32: 
   33: suite() ->
   34:     distributed_helper:require_rpc_nodes([mim]) ++ escalus:suite().
   35: 
   36: %%--------------------------------------------------------------------
   37: %% Init & teardown
   38: %%--------------------------------------------------------------------
   39: 
   40: init_per_suite(Config) ->
   41:     start_http_listener(),
   42:     start_pool(),
   43:     setup_modules(),
   44:     escalus:init_per_suite(Config).
   45: 
   46: end_per_suite(Config) ->
   47:     stop_pool(),
   48:     stop_http_listener(),
   49:     teardown_modules(),
   50:     escalus_fresh:clean(),
   51:     escalus:end_per_suite(Config).
   52: 
   53: init_per_group(GroupName, Config) ->
   54:     Config2 = dynamic_modules:save_modules(domain(), Config),
   55:     dynamic_modules:ensure_modules(domain(), required_modules(GroupName)),
   56:     escalus:create_users(Config2, escalus:get_users([alice, bob])).
   57: 
   58: end_per_group(_, Config) ->
   59:     dynamic_modules:restore_modules(Config),
   60:     escalus:delete_users(Config, escalus:get_users([alice, bob])).
   61: 
   62: init_per_testcase(CaseName, Config) ->
   63:     create_events_collection(),
   64:     escalus:init_per_testcase(CaseName, Config).
   65: 
   66: end_per_testcase(CaseName, Config) ->
   67:     clear_events_collection(),
   68:     escalus:end_per_testcase(CaseName, Config).
   69: 
   70: required_modules(single) ->
   71:     [{mod_event_pusher,
   72:         [{backends,
   73:             [{http,
   74:                 [{path, "/push"},
   75:                     {pool_name, http_pool}]
   76:                 }]
   77:         }]
   78:     }];
   79: required_modules(customised) ->
   80:     [{mod_event_pusher,
   81:         [{backends,
   82:             [{http,
   83:                 [{path, "/push"},
   84:                     {callback_module, mod_event_pusher_http_custom},
   85:                     {pool_name, http_pool}]
   86:                 }]
   87:         }]
   88:     }];
   89: required_modules(multiple) ->
   90:     [{mod_event_pusher,
   91:         [{backends,
   92:             [{http,
   93:                 [{path, "/push"},
   94:                  {callback_module, mod_event_pusher_http_custom},
   95:                  {pool_name, http_pool}]
   96:              },
   97:              {http,
   98:                 [{path, "/push2"},
   99:                  {callback_module, mod_event_pusher_http_custom_2},
  100:                  {pool_name, http_pool}]
  101:             }]
  102:         }]
  103:      }].
  104: 
  105: %%--------------------------------------------------------------------
  106: %% Tests
  107: %%--------------------------------------------------------------------
  108: 
  109: simple_push(Config) ->
  110:     escalus:fresh_story(
  111:         Config, [{alice, 1}, {bob, 1}],
  112:         fun(Alice, Bob) ->
  113:             send(Alice, Bob, <<"hej">>),
  114:             [R] = got_push(push, 1),
  115:             check_default_format(Alice, Bob, <<"hej">>, R),
  116:             send(Alice, Bob, <<>>),
  117:             got_no_push(push),
  118:             ok
  119:         end).
  120: 
  121: custom_push(Config) ->
  122:     escalus:fresh_story(
  123:         Config, [{alice, 1}, {bob, 1}],
  124:         fun(Alice, Bob) ->
  125:             send(Alice, Bob, <<"hej">>),
  126:             send(Alice, Bob, <<>>),
  127:             % now we receive them both ways, and with a custom body
  128:             Res = got_push(push, 4),
  129:             ?assertEqual([<<"in-">>,<<"in-hej">>,<<"out-">>,<<"out-hej">>], lists:sort(Res)),
  130:             ok
  131:         end).
  132: 
  133: push_to_many(Config) ->
  134:     escalus:fresh_story(
  135:         Config, [{alice, 1}, {bob, 1}],
  136:         fun(Alice, Bob) ->
  137:             send(Alice, Bob, <<"hej">>),
  138:             send(Alice, Bob, <<>>),
  139:             % now we receive them both ways, and with a custom body
  140:             Res = got_push(push, 4),
  141:             ?assertEqual([<<"in-">>,<<"in-hej">>,<<"out-">>,<<"out-hej">>], lists:sort(Res)),
  142:             % while the other backend sends only those 'out'
  143:             Res2 = got_push(push2, 2),
  144:             ?assertEqual([<<"2-out-">>,<<"2-out-hej">>], lists:sort(Res2)),
  145:             ok
  146:         end).
  147: 
  148: %%--------------------------------------------------------------------
  149: %% Libs
  150: %%--------------------------------------------------------------------
  151: 
  152: got_no_push(Type) ->
  153:     ?assertEqual(0, length(ets:lookup(?ETS_TABLE, {got_http_push, Type})), unwanted_push).
  154: 
  155: got_push(Type, Count)->
  156:     Key = {got_http_push, Type},
  157:     mongoose_helper:wait_until(
  158:       fun() -> length(ets:lookup(?ETS_TABLE, Key)) end,
  159:       Count, #{name => http_request_timeout}),
  160:     Bins = lists:map(fun({_, El}) -> El end, ets:lookup(?ETS_TABLE, Key)),
  161:     ?assertEqual(Count, length(Bins)), % Assert that this didn't magically grow in the meantime
  162:     ets:delete(?ETS_TABLE, Key),
  163:     Bins.
  164: 
  165: create_events_collection() ->
  166:     ets:new(?ETS_TABLE, [duplicate_bag, named_table, public]).
  167: 
  168: clear_events_collection() ->
  169:     ets:delete_all_objects(?ETS_TABLE).
  170: 
  171: start_http_listener() ->
  172:     http_helper:start(http_notifications_port(), '_', fun process_notification/1).
  173: 
  174: stop_http_listener() ->
  175:     http_helper:stop().
  176: 
  177: process_notification(Req) ->
  178:     <<$/, BType/binary>> = cowboy_req:path(Req),
  179:     Type = binary_to_atom(BType, utf8),
  180:     {ok, Body, Req1} = cowboy_req:read_body(Req),
  181:     Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, <<"OK">>, Req1),
  182:     Event = {{got_http_push, Type}, Body},
  183:     ets:insert(?ETS_TABLE, Event),
  184:     Req2.
  185: 
  186: check_default_format(From, To, Body, Msg) ->
  187:     Attrs = lists:map(fun(P) -> list_to_tuple(binary:split(P, <<$=>>)) end, binary:split(Msg, <<$&>>, [global])),
  188:     ?assertEqual(to_lower(escalus_client:username(From)), proplists:get_value(<<"author">>, Attrs)),
  189:     ?assertEqual(to_lower(escalus_client:username(To)), proplists:get_value(<<"receiver">>, Attrs)),
  190:     ?assertEqual(Body, proplists:get_value(<<"message">>, Attrs)),
  191:     ?assertEqual(<<"localhost">>, proplists:get_value(<<"server">>, Attrs)),
  192:     ok.
  193: 
  194: start_pool() ->
  195:     PoolOpts = #{strategy => random_worker, call_timeout => 5000, workers => 10},
  196:     HTTPOpts = #{path_prefix => "/", http_opts => [], server => http_notifications_host(),
  197:                  request_timeout => 5000},
  198:     Pool = #{type => http, scope => host, tag => http_pool, opts => PoolOpts, conn_opts => HTTPOpts},
  199:     ejabberd_node_utils:call_fun(mongoose_wpool, start_configured_pools,
  200:                                  [[Pool], [<<"localhost">>]]).
  201: 
  202: stop_pool() ->
  203:     ejabberd_node_utils:call_fun(mongoose_wpool, stop, [http, <<"localhost">>, http_pool]).
  204: 
  205: setup_modules() ->
  206:     {Mod, Code} = rpc(dynamic_compile, from_string, [custom_module_code()]),
  207:     rpc(code, load_binary, [Mod, "mod_event_pusher_http_custom.erl", Code]),
  208:     {Mod2, Code2} = rpc(dynamic_compile, from_string, [custom_module_code_2()]),
  209:     rpc(code, load_binary, [Mod2, "mod_event_pusher_http_custom_2.erl", Code2]),
  210:     ok.
  211: 
  212: teardown_modules() ->
  213:     ok.
  214: 
  215: rpc(M, F, A) ->
  216:     distributed_helper:rpc(distributed_helper:mim(), M, F, A).
  217: 
  218: custom_module_code() ->
  219:     "-module(mod_event_pusher_http_custom).
  220:      -export([should_make_req/6, prepare_body/7, prepare_headers/7]).
  221:      should_make_req(Acc, _, _, _, _, _) ->
  222:          case mongoose_acc:stanza_name(Acc) of
  223:              <<\"message\">> -> true;
  224:              _ -> false
  225:          end.
  226:      prepare_headers(_, _, _, _, _, _, _) ->
  227:          mod_event_pusher_http_defaults:prepare_headers(x, x, x, x, x, x, x).
  228:      prepare_body(_Acc, Dir, _Host, Message, _Sender, _Receiver, _Opts) ->
  229:          <<(atom_to_binary(Dir, utf8))/binary, $-, Message/binary>>.
  230:      "
  231: .
  232: 
  233: custom_module_code_2() ->
  234:     "-module(mod_event_pusher_http_custom_2).
  235:      -export([should_make_req/6, prepare_body/7, prepare_headers/7]).
  236:      should_make_req(Acc, out, _, _, _, _) ->
  237:          case mongoose_acc:stanza_name(Acc) of
  238:              <<\"message\">> -> true;
  239:              _ -> false
  240:          end;
  241:      should_make_req(_, in, _, _, _, _) -> false.
  242:      prepare_headers(_, _, _, _, _, _, _) ->
  243:          mod_event_pusher_http_defaults:prepare_headers(x, x, x, x, x, x, x).
  244:      prepare_body(_Acc, Dir, _Host, Message, _Sender, _Receiver, _Opts) ->
  245:          <<$2, $-, (atom_to_binary(Dir, utf8))/binary, $-, Message/binary>>.
  246:      "
  247:     .
  248: 
  249: to_lower(B) ->
  250:     list_to_binary(
  251:         string:to_lower(
  252:             binary_to_list(
  253:                 B
  254:             )
  255:         )
  256:     ).
  257: 
  258: send(Alice, Bob, Body) ->
  259:     Stanza = escalus_stanza:chat_to(Bob, Body),
  260:     escalus_client:send(Alice, Stanza).