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: -import(domain_helper, [domain/0]).
   14: -import(config_parser_helper, [mod_event_pusher_http_handler/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(GroupName) ->
   71:     [{mod_event_pusher, #{http => #{handlers => push_http_handler_opts(GroupName)}}}].
   72: 
   73: push_http_handler_opts(GroupName) ->
   74:     BasicOpts = mod_event_pusher_http_handler(),
   75:     [maps:merge(BasicOpts, ExtraOpts) || ExtraOpts <- push_http_handler_extra_opts(GroupName)].
   76: 
   77: push_http_handler_extra_opts(single) ->
   78:     [#{path => <<"push">>}];
   79: push_http_handler_extra_opts(customised) ->
   80:     [#{path => <<"push">>, callback_module => mod_event_pusher_http_custom}];
   81: push_http_handler_extra_opts(multiple) ->
   82:     [#{path => <<"push">>, callback_module => mod_event_pusher_http_custom},
   83:      #{path => <<"push2">>, callback_module => mod_event_pusher_http_custom_2}].
   84: 
   85: %%--------------------------------------------------------------------
   86: %% Tests
   87: %%--------------------------------------------------------------------
   88: 
   89: simple_push(Config) ->
   90:     escalus:fresh_story(
   91:         Config, [{alice, 1}, {bob, 1}],
   92:         fun(Alice, Bob) ->
   93:             send(Alice, Bob, <<"hej">>),
   94:             [R] = got_push(push, 1),
   95:             check_default_format(Alice, Bob, <<"hej">>, R),
   96:             send(Alice, Bob, <<>>),
   97:             got_no_push(push),
   98:             ok
   99:         end).
  100: 
  101: custom_push(Config) ->
  102:     escalus:fresh_story(
  103:         Config, [{alice, 1}, {bob, 1}],
  104:         fun(Alice, Bob) ->
  105:             send(Alice, Bob, <<"hej">>),
  106:             send(Alice, Bob, <<>>),
  107:             % now we receive them both ways, and with a custom body
  108:             Res = got_push(push, 4),
  109:             ?assertEqual([<<"in-">>,<<"in-hej">>,<<"out-">>,<<"out-hej">>], lists:sort(Res)),
  110:             ok
  111:         end).
  112: 
  113: push_to_many(Config) ->
  114:     escalus:fresh_story(
  115:         Config, [{alice, 1}, {bob, 1}],
  116:         fun(Alice, Bob) ->
  117:             send(Alice, Bob, <<"hej">>),
  118:             send(Alice, Bob, <<>>),
  119:             % now we receive them both ways, and with a custom body
  120:             Res = got_push(push, 4),
  121:             ?assertEqual([<<"in-">>,<<"in-hej">>,<<"out-">>,<<"out-hej">>], lists:sort(Res)),
  122:             % while the other backend sends only those 'out'
  123:             Res2 = got_push(push2, 2),
  124:             ?assertEqual([<<"2-out-">>,<<"2-out-hej">>], lists:sort(Res2)),
  125:             ok
  126:         end).
  127: 
  128: %%--------------------------------------------------------------------
  129: %% Libs
  130: %%--------------------------------------------------------------------
  131: 
  132: got_no_push(Type) ->
  133:     ?assertEqual(0, length(ets:lookup(?ETS_TABLE, {got_http_push, Type})), unwanted_push).
  134: 
  135: got_push(Type, Count)->
  136:     Key = {got_http_push, Type},
  137:     mongoose_helper:wait_until(
  138:       fun() -> length(ets:lookup(?ETS_TABLE, Key)) end,
  139:       Count, #{name => http_request_timeout}),
  140:     Bins = lists:map(fun({_, El}) -> El end, ets:lookup(?ETS_TABLE, Key)),
  141:     ?assertEqual(Count, length(Bins)), % Assert that this didn't magically grow in the meantime
  142:     ets:delete(?ETS_TABLE, Key),
  143:     Bins.
  144: 
  145: create_events_collection() ->
  146:     ets:new(?ETS_TABLE, [duplicate_bag, named_table, public]).
  147: 
  148: clear_events_collection() ->
  149:     ets:delete_all_objects(?ETS_TABLE).
  150: 
  151: start_http_listener() ->
  152:     http_helper:start(http_notifications_port(), '_', fun process_notification/1).
  153: 
  154: stop_http_listener() ->
  155:     http_helper:stop().
  156: 
  157: process_notification(Req) ->
  158:     <<$/, BType/binary>> = cowboy_req:path(Req),
  159:     Type = binary_to_atom(BType, utf8),
  160:     {ok, Body, Req1} = cowboy_req:read_body(Req),
  161:     Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"text/plain">>}, <<"OK">>, Req1),
  162:     Event = {{got_http_push, Type}, Body},
  163:     ets:insert(?ETS_TABLE, Event),
  164:     Req2.
  165: 
  166: check_default_format(From, To, Body, Msg) ->
  167:     Attrs = lists:map(fun(P) -> list_to_tuple(binary:split(P, <<$=>>)) end, binary:split(Msg, <<$&>>, [global])),
  168:     ?assertEqual(to_lower(escalus_client:username(From)), proplists:get_value(<<"author">>, Attrs)),
  169:     ?assertEqual(to_lower(escalus_client:username(To)), proplists:get_value(<<"receiver">>, Attrs)),
  170:     ?assertEqual(Body, proplists:get_value(<<"message">>, Attrs)),
  171:     ?assertEqual(<<"localhost">>, proplists:get_value(<<"server">>, Attrs)),
  172:     ok.
  173: 
  174: start_pool() ->
  175:     PoolOpts = #{strategy => random_worker, call_timeout => 5000, workers => 10},
  176:     HTTPOpts = #{path_prefix => "/", http_opts => [], server => http_notifications_host(),
  177:                  request_timeout => 5000},
  178:     Pool = #{type => http, scope => host, tag => http_pool, opts => PoolOpts, conn_opts => HTTPOpts},
  179:     ejabberd_node_utils:call_fun(mongoose_wpool, start_configured_pools,
  180:                                  [[Pool], [<<"localhost">>]]).
  181: 
  182: stop_pool() ->
  183:     ejabberd_node_utils:call_fun(mongoose_wpool, stop, [http, <<"localhost">>, http_pool]).
  184: 
  185: setup_modules() ->
  186:     {Mod, Code} = rpc(dynamic_compile, from_string, [custom_module_code()]),
  187:     rpc(code, load_binary, [Mod, "mod_event_pusher_http_custom.erl", Code]),
  188:     {Mod2, Code2} = rpc(dynamic_compile, from_string, [custom_module_code_2()]),
  189:     rpc(code, load_binary, [Mod2, "mod_event_pusher_http_custom_2.erl", Code2]),
  190:     ok.
  191: 
  192: teardown_modules() ->
  193:     ok.
  194: 
  195: rpc(M, F, A) ->
  196:     distributed_helper:rpc(distributed_helper:mim(), M, F, A).
  197: 
  198: custom_module_code() ->
  199:     "-module(mod_event_pusher_http_custom).
  200:      -export([should_make_req/6, prepare_body/7, prepare_headers/7]).
  201:      should_make_req(Acc, _, _, _, _, _) ->
  202:          case mongoose_acc:stanza_name(Acc) of
  203:              <<\"message\">> -> true;
  204:              _ -> false
  205:          end.
  206:      prepare_headers(_, _, _, _, _, _, _) ->
  207:          mod_event_pusher_http_defaults:prepare_headers(x, x, x, x, x, x, x).
  208:      prepare_body(_Acc, Dir, _Host, Message, _Sender, _Receiver, _Opts) ->
  209:          <<(atom_to_binary(Dir, utf8))/binary, $-, Message/binary>>.
  210:      "
  211: .
  212: 
  213: custom_module_code_2() ->
  214:     "-module(mod_event_pusher_http_custom_2).
  215:      -export([should_make_req/6, prepare_body/7, prepare_headers/7]).
  216:      should_make_req(Acc, out, _, _, _, _) ->
  217:          case mongoose_acc:stanza_name(Acc) of
  218:              <<\"message\">> -> true;
  219:              _ -> false
  220:          end;
  221:      should_make_req(_, in, _, _, _, _) -> false.
  222:      prepare_headers(_, _, _, _, _, _, _) ->
  223:          mod_event_pusher_http_defaults:prepare_headers(x, x, x, x, x, x, x).
  224:      prepare_body(_Acc, Dir, _Host, Message, _Sender, _Receiver, _Opts) ->
  225:          <<$2, $-, (atom_to_binary(Dir, utf8))/binary, $-, Message/binary>>.
  226:      "
  227:     .
  228: 
  229: to_lower(B) ->
  230:     list_to_binary(
  231:         string:to_lower(
  232:             binary_to_list(
  233:                 B
  234:             )
  235:         )
  236:     ).
  237: 
  238: send(Alice, Bob, Body) ->
  239:     Stanza = escalus_stanza:chat_to(Bob, Body),
  240:     escalus_client:send(Alice, Stanza).