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).