1: %%==============================================================================
    2: %% Copyright 2015 Erlang Solutions Ltd.
    3: %%
    4: %% Licensed under the Apache License, Version 2.0 (the "License");
    5: %% you may not use this file except in compliance with the License.
    6: %% You may obtain a copy of the License at
    7: %%
    8: %% http://www.apache.org/licenses/LICENSE-2.0
    9: %%
   10: %% Unless required by applicable law or agreed to in writing, software
   11: %% distributed under the License is distributed on an "AS IS" BASIS,
   12: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   13: %% See the License for the specific language governing permissions and
   14: %% limitations under the License.
   15: %%==============================================================================
   16: -module(mod_ping_SUITE).
   17: -compile([export_all, nowarn_export_all]).
   18: 
   19: -include_lib("escalus/include/escalus.hrl").
   20: -include_lib("escalus/include/escalus_xmlns.hrl").
   21: -include_lib("exml/include/exml.hrl").
   22: -include_lib("common_test/include/ct.hrl").
   23: 
   24: -import(domain_helper, [domain/0]).
   25: 
   26: %%--------------------------------------------------------------------
   27: %% Suite configuration
   28: %%--------------------------------------------------------------------
   29: all() ->
   30:     [
   31:      {group, client_ping},
   32:      {group, server_ping},
   33:      {group, server_ping_kill}
   34:     ].
   35: 
   36: groups() ->
   37:     % Don't make these parallel! Metrics tests will most probably fail
   38:     % and injected hook will most probably won't work as expected.
   39:     [
   40:      {client_ping, [], [disco, ping]},
   41:      {server_ping, [], all_tests()},
   42:      {server_ping_kill, [], all_tests()}
   43:     ].
   44: 
   45: client_ping_test_cases() ->
   46:     [
   47:      ping,
   48:      wrong_ping
   49:     ].
   50: 
   51: all_tests() ->
   52:     [
   53:      disco,
   54:      ping,
   55:      wrong_ping,
   56:      active,
   57:      active_keep_alive,
   58:      server_ping_pong,
   59:      server_ping_pang,
   60:      service_unavailable_response
   61:     ].
   62: 
   63: suite() ->
   64:     escalus:suite().
   65: 
   66: ping_interval() ->
   67:     timer:seconds(3).
   68: 
   69: ping_req_timeout() ->
   70:     timer:seconds(2).
   71: 
   72: init_per_suite(Config) ->
   73:     mongoose_helper:inject_module(?MODULE),
   74:     escalus:init_per_suite(Config).
   75: 
   76: end_per_suite(Config) ->
   77:     escalus_fresh:clean(),
   78:     escalus:end_per_suite(Config).
   79: 
   80: init_per_group(client_ping, Config) ->
   81:     start_mod_ping(#{}),
   82:     Config;
   83: init_per_group(server_ping, Config) ->
   84:     start_mod_ping(#{send_pings => true,
   85:                      ping_interval => ping_interval(),
   86:                      ping_req_timeout => ping_req_timeout()}),
   87:     Config;
   88: init_per_group(server_ping_kill, Config) ->
   89:     start_mod_ping(#{send_pings => true,
   90:                      ping_interval => ping_interval(),
   91:                      ping_req_timeout => ping_req_timeout(),
   92:                      timeout_action => kill}),
   93:     [{timeout_action, kill} | Config].
   94: 
   95: end_per_group(_GroupName, Config) ->
   96:     HostType = domain_helper:host_type(mim),
   97:     dynamic_modules:stop(HostType, mod_ping),
   98:     Config.
   99: 
  100: init_per_testcase(server_ping_pong = CN, Config) ->
  101:     NConfig = setup_pong_hook(Config),
  102:     escalus:init_per_testcase(CN, NConfig);
  103: init_per_testcase(CaseName, Config) ->
  104:     escalus:init_per_testcase(CaseName, Config).
  105: 
  106: end_per_testcase(server_ping_pong = CN, Config) ->
  107:     NConfig = clear_pong_hook(Config),
  108:     escalus:init_per_testcase(CN, NConfig);
  109: end_per_testcase(CaseName, Config) ->
  110:     escalus:end_per_testcase(CaseName, Config).
  111: 
  112: start_mod_ping(Opts) ->
  113:     HostType = domain_helper:host_type(mim),
  114:     dynamic_modules:start(HostType, mod_ping, config_parser_helper:mod_config(mod_ping, Opts)).
  115: 
  116: setup_pong_hook(Config) ->
  117:     Pid = self(),
  118:     HostType = domain_helper:host_type(mim),
  119:     mongoose_helper:successful_rpc(?MODULE, setup_pong_hook, [HostType, Pid]),
  120:     [{pid, Pid} | Config].
  121: 
  122: setup_pong_hook(HostType, Pid) ->
  123:     gen_hook:add_handler(user_ping_response, HostType,
  124:                          fun ?MODULE:pong_hook_handler/3,
  125:                          #{pid => Pid}, 50).
  126: 
  127: pong_hook_handler(Acc,
  128:                   #{jid := JID} = _Params,
  129:                   #{pid := Pid} = _Extra) ->
  130:     Pid ! {pong, jid:to_binary(jid:to_lower(JID))},
  131:     {ok, Acc}.
  132: 
  133: clear_pong_hook(Config) ->
  134:     {value, {_, Pid}, NConfig} = lists:keytake(pid, 1, Config),
  135:     HostType = domain_helper:host_type(mim),
  136:     mongoose_helper:successful_rpc(?MODULE, clear_pong_hook, [HostType, Pid]),
  137:     NConfig.
  138: 
  139: clear_pong_hook(HostType, Pid) ->
  140:     gen_hook:delete_handler(user_ping_response, HostType,
  141:                             fun ?MODULE:pong_hook_handler/3,
  142:                             #{pid => Pid}, 50).
  143: 
  144: %%--------------------------------------------------------------------
  145: %% Ping tests
  146: %%--------------------------------------------------------------------
  147: disco(Config) ->
  148:     escalus:fresh_story(
  149:       Config, [{alice, 1}],
  150:       fun(Alice) ->
  151:               escalus_client:send(Alice, escalus_stanza:disco_info(domain())),
  152:               Response = escalus_client:wait_for_stanza(Alice),
  153:               escalus:assert(has_feature, [?NS_PING], Response)
  154:       end).
  155: 
  156: ping(ConfigIn) ->
  157:     Domain = domain(),
  158:     HostType = domain_helper:host_type(mim),
  159:     HostTypePrefix = domain_helper:make_metrics_prefix(HostType),
  160:     Metrics = [
  161:         {[HostTypePrefix, mod_ping, ping_response],0},
  162:         {[HostTypePrefix, mod_ping, ping_response_timeout],0}
  163:     ],
  164:     Config = [{mongoose_metrics, Metrics} | ConfigIn],
  165:     escalus:fresh_story(Config, [{alice, 1}],
  166:         fun(Alice) ->
  167:                 PingReq = escalus_stanza:ping_request(Domain),
  168:                 escalus_client:send(Alice, PingReq),
  169: 
  170:                 PingResp = escalus_client:wait_for_stanza(Alice),
  171:                 escalus:assert(is_iq_result, [PingReq], PingResp)
  172:         end).
  173: 
  174: wrong_ping(Config) ->
  175:     escalus:fresh_story(Config, [{alice, 1}],
  176:         fun(Alice) ->
  177:             Domain = domain(),
  178:             IQ = escalus_stanza:iq(<<"get">>, [#xmlel{name = <<"unsupported">>,
  179:                                                       attrs = [{<<"xmlns">>, ?NS_PING}]
  180:             }]),
  181:             PingReq = escalus_stanza:to(IQ, Domain),
  182:             escalus_client:send(Alice, PingReq),
  183: 
  184:             PingResp = escalus_client:wait_for_stanza(Alice),
  185:             escalus:assert(is_iq_error, [PingReq], PingResp)
  186:         end).
  187: 
  188: service_unavailable_response(Config) ->
  189:     escalus:fresh_story(Config, [{alice, 1}],
  190:         fun(Alice) ->
  191:             PingReq = wait_for_ping_req(Alice),
  192:             PingId = exml_query:attr(PingReq, <<"id">>),
  193: 
  194:             ErrorStanzaBody = [#xmlel{name = <<"ping">>, attrs = [{<<"xmlns">>, ?NS_PING}]},
  195:                                #xmlel{name = <<"error">>, attrs = [{<<"type">>, <<"cancel">>}],
  196:                                children = [#xmlel{name = <<"service-unavailable">>,
  197:                                                   attrs = [{<<"xmlns">>, ?NS_STANZA_ERRORS}]}]}],
  198:             ErrorStanza = escalus_stanza:set_id(
  199:                             escalus_stanza:iq(domain(), <<"error">>, ErrorStanzaBody), PingId),
  200:             escalus_client:send(Alice, ErrorStanza),
  201: 
  202:             TimeoutAction = ?config(timeout_action, Config),
  203:             check_connection(TimeoutAction, Alice),
  204:             escalus_client:kill_connection(Config, Alice)
  205:         end).
  206: 
  207: active(ConfigIn) ->
  208:     Domain = domain(),
  209:     HostType = domain_helper:host_type(mim),
  210:     HostTypePrefix = domain_helper:make_metrics_prefix(HostType),
  211:     Metrics = [
  212:         {[HostTypePrefix, mod_ping, ping_response],0},
  213:         {[HostTypePrefix, mod_ping, ping_response_timeout],0}
  214:     ],
  215:     Config = [{mongoose_metrics, Metrics} | ConfigIn],
  216:     escalus:fresh_story(Config, [{alice, 1}],
  217:         fun(Alice) ->
  218:                 wait_ping_interval(0.75),
  219:                 escalus_client:send(Alice, escalus_stanza:ping_request(Domain)),
  220:                 escalus:assert(is_iq_result, escalus_client:wait_for_stanza(Alice)),
  221:                 % wait more time and check if connection got ping req
  222:                 wait_ping_interval(0.5),
  223:                 % it shouldn't as the ping was sent
  224:                 false = escalus_client:has_stanzas(Alice)
  225:         end).
  226: 
  227: active_keep_alive(ConfigIn) ->
  228:     HostType = domain_helper:host_type(mim),
  229:     HostTypePrefix = domain_helper:make_metrics_prefix(HostType),
  230:     Metrics = [
  231:         {[HostTypePrefix, mod_ping, ping_response],0},
  232:         {[HostTypePrefix, mod_ping, ping_response_timeout],0}
  233:     ],
  234:     Config = [{mongoose_metrics, Metrics} | ConfigIn],
  235:     escalus:fresh_story(Config, [{alice, 1}],
  236:         fun(Alice) ->
  237:                 wait_ping_interval(0.75),
  238:                 escalus_tcp:send(Alice#client.rcv_pid, #xmlcdata{content = "\n"}),
  239:                 wait_ping_interval(0.5),
  240: 
  241:                 false = escalus_client:has_stanzas(Alice)
  242:         end).
  243: 
  244: server_ping_pong(ConfigIn) ->
  245:     HostType = domain_helper:host_type(mim),
  246:     HostTypePrefix = domain_helper:make_metrics_prefix(HostType),
  247:     Metrics = [
  248:         {[HostTypePrefix, mod_ping, ping_response], 5},
  249:         {[HostTypePrefix, mod_ping, ping_response_timeout], 0},
  250:         {[HostTypePrefix, mod_ping, ping_response_time], changed}
  251:     ],
  252:     Config = [{mongoose_metrics, Metrics} | ConfigIn],
  253:     %% We use 5 Alices because with just 1 sample the histogram may look like it hasn't changed
  254:     %% due to exometer histogram implementation
  255:     escalus:fresh_story(Config, [{alice, 5}],
  256:         fun(Alice1, Alice2, Alice3, Alice4, Alice5) ->
  257:                 lists:foreach(fun(Alice) ->
  258:                                       PingReq = wait_for_ping_req(Alice),
  259:                                       Pong = escalus_stanza:iq_result(PingReq),
  260:                                       escalus_client:send(Alice, Pong)
  261:                               end, [Alice1, Alice2, Alice3, Alice4, Alice5]),
  262:                 wait_for_pong_hooks(5)
  263:         end).
  264: 
  265: server_ping_pang(ConfigIn) ->
  266:     HostType = domain_helper:host_type(mim),
  267:     HostTypePrefix = domain_helper:make_metrics_prefix(HostType),
  268:     Metrics = [
  269:         {[HostTypePrefix, mod_ping, ping_response], 0},
  270:         {[HostTypePrefix, mod_ping, ping_response_timeout], 1}
  271:     ],
  272:     Config = [{mongoose_metrics, Metrics} | ConfigIn],
  273:     escalus:fresh_story(Config, [{alice, 1}],
  274:         fun(Alice) ->
  275:                 wait_for_ping_req(Alice),
  276:                 %% do not resp to ping req
  277:                 ct:sleep(ping_req_timeout() + timer:seconds(1)/2),
  278:                 TimeoutAction = ?config(timeout_action, Config),
  279:                 check_connection(TimeoutAction, Alice),
  280:                 escalus_client:kill_connection(Config, Alice)
  281:         end).
  282: 
  283: wait_ping_interval(Ration) ->
  284:     WaitTime = ping_interval() * Ration,
  285:     ct:sleep(WaitTime).
  286: 
  287: check_connection(kill, Client) ->
  288:     mongoose_helper:wait_until(fun() -> escalus_connection:is_connected(Client) end, false);
  289: check_connection(_, Client) ->
  290:     true = escalus_connection:is_connected(Client).
  291: 
  292: wait_for_ping_req(Alice) ->
  293:     PingReq = escalus_client:wait_for_stanza(Alice, timer:seconds(10)),
  294:     escalus:assert(is_iq_get, PingReq),
  295:     <<"urn:xmpp:ping">> = exml_query:path(PingReq, [{element, <<"ping">>},
  296:                                                     {attr, <<"xmlns">>}]),
  297:     PingReq.
  298: 
  299: wait_for_pong_hooks(0) ->
  300:     ok;
  301: wait_for_pong_hooks(N) ->
  302:     receive
  303:         {pong, _} -> wait_for_pong_hooks(N-1)
  304:     after
  305:         5000 ->
  306:             ct:fail({pong_hook_runs_missing, N})
  307:     end.