1: %%==============================================================================
    2: %% Copyright 2012 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: 
   17: -module(bosh_SUITE).
   18: -compile([export_all, nowarn_export_all]).
   19: 
   20: -include_lib("escalus/include/escalus.hrl").
   21: -include_lib("common_test/include/ct.hrl").
   22: -include_lib("eunit/include/eunit.hrl").
   23: -include_lib("exml/include/exml.hrl").
   24: 
   25: -import(distributed_helper, [mim/0,
   26:                              require_rpc_nodes/1,
   27:                              rpc/4]).
   28: -import(domain_helper, [host_type/0, domain/0]).
   29: 
   30: %%--------------------------------------------------------------------
   31: %% Suite configuration
   32: %%--------------------------------------------------------------------
   33: 
   34: -define(INACTIVITY, 2). %% seconds
   35: -define(MAX_WAIT, 5). %% seconds
   36: -define(INVALID_RID_OFFSET, 999).
   37: 
   38: all() ->
   39:     [
   40:      {group, without_bosh},
   41:      {group, essential},
   42:      {group, essential_https},
   43:      {group, chat},
   44:      {group, chat_https},
   45:      {group, time},
   46:      {group, acks},
   47:      {group, server_acks},
   48:      {group, interleave_requests_statem}
   49:     ].
   50: 
   51: groups() ->
   52:     [
   53:      {without_bosh, [], [reject_connection_when_mod_bosh_is_disabled]},
   54:      {essential, [sequence], essential_test_cases()},
   55:      {essential_https, [sequence], essential_test_cases()},
   56:      {chat, [shuffle], chat_test_cases()},
   57:      {chat_https, [shuffle], chat_test_cases()},
   58:      {time, [parallel], time_test_cases()},
   59:      {acks, [shuffle], acks_test_cases()},
   60:      {server_acks, [], [server_acks]},
   61:      {interleave_requests_statem, [parallel], [interleave_requests_statem]}
   62:     ].
   63: 
   64: suite() ->
   65:     require_rpc_nodes([mim]) ++ escalus:suite().
   66: 
   67: essential_test_cases() ->
   68:     [create_and_terminate_session,
   69:      accept_higher_hold_value,
   70:      do_not_accept_0_hold_value,
   71:      options_request,
   72:      get_request,
   73:      post_empty_body,
   74:      put_request].
   75: 
   76: chat_test_cases() ->
   77:     [
   78:      interleave_requests,
   79:      simple_chat,
   80:      cdata_escape_chat,
   81:      escape_attr_chat,
   82:      cant_send_invalid_rid,
   83:      multiple_stanzas,
   84:      namespace,
   85:      stream_error
   86:     ].
   87: 
   88: time_test_cases() ->
   89:     [disconnect_inactive,
   90:      connection_interrupted,
   91:      interrupt_long_poll_is_activity,
   92:      reply_on_pause,
   93:      cant_pause_for_too_long,
   94:      pause_request_is_activity,
   95:      reply_in_time
   96:     ].
   97: 
   98: acks_test_cases() ->
   99:     [
  100:      force_report,
  101:      force_retransmission,
  102:      force_cache_trimming
  103:     ].
  104: 
  105: %%--------------------------------------------------------------------
  106: %% Init & teardown
  107: %%--------------------------------------------------------------------
  108: 
  109: init_per_suite(Config) ->
  110:     Config1 = dynamic_modules:save_modules(host_type(), Config),
  111:     escalus:init_per_suite([{escalus_user_db, {module, escalus_ejabberd}} | Config1]).
  112: 
  113: end_per_suite(Config) ->
  114:     dynamic_modules:restore_modules(Config),
  115:     escalus_fresh:clean(),
  116:     escalus:end_per_suite(Config).
  117: 
  118: init_per_group(time, Config) ->
  119:     dynamic_modules:ensure_modules(host_type(), required_modules(time)),
  120:     Config;
  121: init_per_group(GroupName, Config) when GroupName =:= essential;
  122:                                        GroupName =:= without_bosh ->
  123:     dynamic_modules:ensure_modules(host_type(), required_modules(GroupName)),
  124:     [{user, carol} | Config];
  125: init_per_group(essential_https, Config) ->
  126:     dynamic_modules:ensure_modules(host_type(), required_modules(essential_https)),
  127:     [{user, carol_s} | Config];
  128: init_per_group(chat_https, Config) ->
  129:     dynamic_modules:ensure_modules(host_type(), required_modules(chat_https)),
  130:     Config1 = escalus:create_users(Config, escalus:get_users([carol, carol_s, geralt, alice])),
  131:     [{user, carol_s} | Config1];
  132: init_per_group(GroupName, Config) ->
  133:     dynamic_modules:ensure_modules(host_type(), required_modules(GroupName)),
  134:     Config1 = escalus:create_users(Config, escalus:get_users([carol, carol_s, geralt, alice])),
  135:     [{user, carol} | Config1].
  136: 
  137: end_per_group(GroupName, _Config) when GroupName =:= time;
  138:                                        GroupName =:= essential;
  139:                                        GroupName =:= without_bosh;
  140:                                        GroupName =:= essential_https ->
  141:     ok;
  142: end_per_group(_GroupName, Config) ->
  143:     escalus:delete_users(Config, escalus:get_users([carol, carol_s, geralt, alice])),
  144:     mongoose_helper:clear_last_activity(Config, carol).
  145: 
  146: init_per_testcase(CaseName, Config) ->
  147:     escalus:init_per_testcase(CaseName, Config).
  148: 
  149: end_per_testcase(CaseName, Config) ->
  150:     escalus:end_per_testcase(CaseName, Config).
  151: 
  152: %% Module configuration per group
  153: 
  154: required_modules(without_bosh) ->
  155:     [{mod_bosh, stopped}];
  156: required_modules(GroupName) ->
  157:     [{mod_bosh, maps:merge(config_parser_helper:default_mod_config(mod_bosh),
  158:                            required_bosh_opts(GroupName))}].
  159: 
  160: required_bosh_opts(time) ->
  161:     #{max_wait => ?MAX_WAIT, inactivity => ?INACTIVITY};
  162: required_bosh_opts(server_acks) ->
  163:     #{server_acks => true};
  164: required_bosh_opts(_Group) ->
  165:     #{}.
  166: 
  167: %%--------------------------------------------------------------------
  168: %% Tests
  169: %%--------------------------------------------------------------------
  170: 
  171: create_and_terminate_session(Config) ->
  172:     MongooseMetrics = [{[global, data, xmpp, received, xml_stanza_size], changed},
  173:                        {[global, data, xmpp, sent, xml_stanza_size], changed},
  174:                        {[global, data, xmpp, received, c2s, bosh], changed},
  175:                        {[global, data, xmpp, sent, c2s, bosh], changed},
  176:                        {[global, data, xmpp, received, c2s, tcp], 0},
  177:                        {[global, data, xmpp, sent, c2s, tcp], 0}],
  178:     PreStoryData = escalus_mongooseim:pre_story([{mongoose_metrics, MongooseMetrics}]),
  179:     NamedSpecs = escalus_config:get_config(escalus_users, Config),
  180:     CarolSpec = proplists:get_value(?config(user, Config), NamedSpecs),
  181:     Conn = escalus_connection:connect(CarolSpec),
  182: 
  183:     %% Assert there are no BOSH sessions on the server.
  184:     [] = get_bosh_sessions(),
  185: 
  186:     Domain = domain(),
  187:     Body = escalus_bosh:session_creation_body(get_bosh_rid(Conn), Domain),
  188:     ok = bosh_send_raw(Conn, Body),
  189:     escalus_connection:get_stanza(Conn, session_creation_response),
  190: 
  191:     %% Assert that a BOSH session was created.
  192:     [_] = get_bosh_sessions(),
  193: 
  194:     Sid = get_bosh_sid(Conn),
  195:     Terminate = escalus_bosh:session_termination_body(get_bosh_rid(Conn), Sid),
  196:     ok = bosh_send_raw(Conn, Terminate),
  197: 
  198:     escalus_mongooseim:post_story(PreStoryData),
  199: 
  200:     %% Assert the session was terminated.
  201:     wait_for_zero_bosh_sessions().
  202: 
  203: reject_connection_when_mod_bosh_is_disabled(Config) ->
  204:     {Domain, Path, Client} = get_fusco_connection(Config),
  205:     Rid = rand:uniform(1000000),
  206:     Body = escalus_bosh:session_creation_body(2, <<"1.0">>, <<"en">>, Rid, Domain, nil),
  207:     Result = fusco_request(Client, <<"POST">>, Path, exml:to_iolist(Body)),
  208:     {{<<"200">>, <<"OK">>}, _Headers, RespBody, _, _} = Result,
  209:     {ok, #xmlel{attrs = RespAttrs}} = exml:parse(RespBody),
  210: 
  211:     ?assertMatch(#{<<"type">> := <<"terminate">>}, maps:from_list(RespAttrs)),
  212:     ?assertEqual([], get_bosh_sessions()),
  213:     fusco_cp:stop(Client).
  214: 
  215: do_not_accept_0_hold_value(Config) ->
  216:     {Domain, Path, Client} = get_fusco_connection(Config),
  217:     Rid = rand:uniform(1000000),
  218:     Body0 = escalus_bosh:session_creation_body(2, <<"1.0">>, <<"en">>, Rid, Domain, nil),
  219:     #xmlel{attrs = Attrs0} = Body0,
  220:     Attrs = lists:keyreplace(<<"hold">>, 1, Attrs0, {<<"hold">>, <<"0">>}),
  221:     Body = Body0#xmlel{attrs = Attrs},
  222:     Result = fusco_request(Client, <<"POST">>, Path, exml:to_iolist(Body)),
  223:     {{<<"200">>, <<"OK">>}, _Headers, RespBody, _, _} = Result,
  224:     {ok, #xmlel{attrs = RespAttrs}} = exml:parse(RespBody),
  225: 
  226:     ?assertMatch(#{<<"type">> := <<"terminate">>}, maps:from_list(RespAttrs)),
  227:     ?assertEqual([], get_bosh_sessions()),
  228:     fusco_cp:stop(Client).
  229: 
  230: accept_higher_hold_value(Config) ->
  231:     {Domain, Path, Client} = get_fusco_connection(Config),
  232:     Rid = rand:uniform(1000000),
  233:     Body0 = escalus_bosh:session_creation_body(2, <<"1.0">>, <<"en">>, Rid, Domain, nil),
  234:     #xmlel{attrs = Attrs0} = Body0,
  235:     Attrs = lists:keyreplace(<<"hold">>, 1, Attrs0, {<<"hold">>, <<"2">>}),
  236:     Body = Body0#xmlel{attrs = Attrs},
  237:     Result = fusco_request(Client, <<"POST">>, Path, exml:to_iolist(Body)),
  238:     {{<<"200">>, <<"OK">>}, _Headers, RespBody, _, _} = Result,
  239:     {ok, #xmlel{attrs = RespAttrs}} = exml:parse(RespBody),
  240: 
  241:     %% Server returns its hold value, which is 1
  242:     #{<<"hold">> := <<"1">>, <<"sid">> := SID} = maps:from_list(RespAttrs),
  243:     ?assertMatch([_], get_bosh_sessions()),
  244: 
  245:     TerminateBody = escalus_bosh:session_termination_body(Rid + 1, SID),
  246:     Res2 = fusco_request(Client, <<"POST">>, Path, exml:to_iolist(TerminateBody)),
  247:     {{<<"200">>, <<"OK">>}, _, RespBody2, _, _} = Res2,
  248:     {ok, #xmlel{attrs = RespAttrs2}} = exml:parse(RespBody2),
  249:     case maps:from_list(RespAttrs2) of
  250:         #{<<"type">> := <<"terminate">>} ->
  251:             ok;
  252:         #{<<"sid">> := SID} ->
  253:             %% Server sent stream features separately, one more request needed
  254:             EmptyBody = escalus_bosh:empty_body(Rid + 2, SID),
  255:             Res3 = fusco_request(Client, <<"POST">>, Path, exml:to_iolist(EmptyBody)),
  256:             {{<<"200">>, <<"OK">>}, _, RespBody3, _, _} = Res3,
  257:             {ok, #xmlel{attrs = RespAttrs3}} = exml:parse(RespBody3),
  258:             ?assertMatch(#{<<"type">> := <<"terminate">>}, maps:from_list(RespAttrs3))
  259:     end,
  260:     ?assertEqual([], get_bosh_sessions()),
  261:     fusco_cp:stop(Client).
  262: 
  263: fusco_request(Client, Method, Path, Body) ->
  264:     fusco_request(Client, Method, Path, Body, []).
  265: 
  266: fusco_request(Client, Method, Path, Body, HeadersIn) ->
  267:     Headers = [{<<"Content-Type">>, <<"text/xml; charset=utf-8">>} | HeadersIn],
  268:     case fusco_cp:request(Client, Path, Method, Headers, Body, 2, 5000) of
  269:         {ok, Result} ->
  270:             Result;
  271:         Other ->
  272:             ct:fail(#{issue => http_request_failed,
  273:                       reason => Other,
  274:                       client => Client,
  275:                       method => Method,
  276:                       path => Path,
  277:                       body => Body,
  278:                       headers => HeadersIn})
  279:     end.
  280: 
  281: 
  282: options_request(Config) ->
  283:     {Server, Path, Client} = get_fusco_connection(Config),
  284:     Result = fusco_request(Client, <<"OPTIONS">>, Path, <<>>, [{<<"Origin">>, Server}]),
  285:     fusco_cp:stop(Client),
  286:     {{<<"200">>, <<"OK">>}, Headers, <<>>, _, _} = Result,
  287:     <<"1728000">> = proplists:get_value(<<"access-control-max-age">>, Headers),
  288:     <<"content-type">> = proplists:get_value(<<"access-control-allow-headers">>, Headers),
  289:     <<"POST, OPTIONS, GET">> = proplists:get_value(<<"access-control-allow-methods">>, Headers),
  290:     Server = proplists:get_value(<<"access-control-allow-origin">>, Headers).
  291: 
  292: get_request(Config) ->
  293:     {_Server, Path, Client} = get_fusco_connection(Config),
  294:     Result = fusco_request(Client, <<"GET">>, Path, <<>>),
  295:     fusco_cp:stop(Client),
  296:     {{<<"200">>, <<"OK">>}, _, _, _, _} = Result.
  297: 
  298: put_request(Config) ->
  299:     {_Server, Path, Client} = get_fusco_connection(Config),
  300:     Result = fusco_request(Client, <<"PUT">>, Path, <<"not allowed body">>),
  301:     fusco_cp:stop(Client),
  302:     {{<<"405">>, <<"Method Not Allowed">>}, _, _, _, _} = Result.
  303: 
  304: post_empty_body(Config) ->
  305:     {_Server, Path, Client} = get_fusco_connection(Config),
  306:     Result = fusco_request(Client, <<"POST">>, Path, <<>>),
  307:     fusco_cp:stop(Client),
  308:     {{<<"400">>, <<"Bad Request">>}, _, _, _, _} = Result.
  309: 
  310: get_fusco_connection(Config) ->
  311:     NamedSpecs = escalus_config:get_config(escalus_users, Config),
  312:     CarolSpec = proplists:get_value(?config(user, Config), NamedSpecs),
  313:     Server = proplists:get_value(server, CarolSpec),
  314:     Host = proplists:get_value(host, CarolSpec, Server),
  315:     Path = proplists:get_value(path, CarolSpec),
  316:     Port = proplists:get_value(port, CarolSpec),
  317:     UseSSL = proplists:get_value(ssl, CarolSpec, false),
  318:     Opts = case UseSSL of
  319:         true ->
  320:             [{connect_options, [{verify, verify_none}]}];
  321:         false ->
  322:             []
  323:     end,
  324:     {ok, Client} = fusco_cp:start_link({binary_to_list(Host), Port, UseSSL}, Opts, 1),
  325:     {Server, Path, Client}.
  326: 
  327: stream_error(Config) ->
  328:     escalus:story(
  329:       Config, [{?config(user, Config), 1}],
  330:       fun(Carol) ->
  331:               %% Send a stanza with invalid 'from'
  332:               %% attribute to trigger a stream error from
  333:               %% the server.
  334:               Domain = domain(),
  335:               BadMessage = escalus_stanza:chat(
  336:                              <<"not_carol@", Domain/binary>>,
  337:                              <<"geralt@", Domain/binary>>,
  338:                              <<"I am not Carol">>),
  339:               escalus_client:send(Carol, BadMessage),
  340:               escalus:assert(is_stream_error, [<<"invalid-from">>, <<>>],
  341:                              escalus_client:wait_for_stanza(Carol)),
  342:               %% connection should be closed, let's wait
  343:               escalus_client:wait_for_close(Config, Carol, timer:seconds(1))
  344:       end).
  345: 
  346: interleave_requests(Config) ->
  347:     escalus:story(Config, [{geralt, 1}], fun(Geralt) ->
  348: 
  349:         Carol = start_client(Config, ?config(user, Config), <<"bosh">>),
  350:         Rid = get_bosh_rid(Carol),
  351:         Sid = get_bosh_sid(Carol),
  352: 
  353:         Msg1 = <<"1st!">>,
  354:         Msg2 = <<"2nd!">>,
  355:         Msg3 = <<"3rd!">>,
  356:         Msg4 = <<"4th!">>,
  357: 
  358:         send_message_with_rid(Carol, Geralt, Rid + 1, Sid, Msg2),
  359:         send_message_with_rid(Carol, Geralt, Rid, Sid, Msg1),
  360: 
  361:         send_message_with_rid(Carol, Geralt, Rid + 2,   Sid, Msg3),
  362:         send_message_with_rid(Carol, Geralt, Rid + 3,   Sid, Msg4),
  363: 
  364:         escalus:assert(is_chat_message, [Msg1],
  365:                        escalus_client:wait_for_stanza(Geralt)),
  366:         escalus:assert(is_chat_message, [Msg2],
  367:                        escalus_client:wait_for_stanza(Geralt)),
  368:         escalus:assert(is_chat_message, [Msg3],
  369:                        escalus_client:wait_for_stanza(Geralt)),
  370:         escalus:assert(is_chat_message, [Msg4],
  371:                        escalus_client:wait_for_stanza(Geralt)),
  372: 
  373:         true = is_bosh_connected(Carol)
  374:     end).
  375: 
  376: interleave_requests_statem(Config) ->
  377:     %% cancel 30-seconds timetrap, start 5-minutes one
  378:     ct:timetrap({minutes, 5}),
  379:     true = bosh_interleave_reqs:test([{user, carol} | Config]).
  380: 
  381: interleave_requests_statem_https(Config) ->
  382:     interleave_requests_statem([{user, carol_s} | Config]).
  383: 
  384: send_message_with_rid(From, To, Rid, Sid, Msg) ->
  385:     Empty = escalus_bosh:empty_body(Rid, Sid),
  386:     Chat = Empty#xmlel{
  387:              children = [escalus_stanza:chat_to(To, Msg)]},
  388:     bosh_send_raw(From, Chat).
  389: 
  390: 
  391: simple_chat(Config) ->
  392:     escalus:story(Config, [{?config(user, Config), 1}, {geralt, 1}], fun(Carol, Geralt) ->
  393: 
  394:         escalus_client:send(Carol, escalus_stanza:chat_to(Geralt, <<"Hi!">>)),
  395:         escalus:assert(is_chat_message, [<<"Hi!">>],
  396:                        escalus_client:wait_for_stanza(Geralt)),
  397: 
  398:         escalus_client:send(Geralt,
  399:                             escalus_stanza:chat_to(Carol, <<"Hello!">>)),
  400:         escalus:assert(is_chat_message, [<<"Hello!">>],
  401:                        escalus_client:wait_for_stanza(Carol))
  402: 
  403:         end).
  404: 
  405: cdata_escape_chat(Config) ->
  406:     escalus:story(Config, [{?config(user, Config), 1}, {geralt, 1}], fun(Carol, Geralt) ->
  407:         special_chars_helper:check_cdata_from_to(Carol, Carol, <<"Hi! & < > ">>),
  408:         special_chars_helper:check_cdata_from_to(Geralt, Carol, <<"Hi there! & < > ">>)
  409: 
  410:     end).
  411: 
  412: escape_attr_chat(Config) ->
  413:     escalus:story(Config, [{?config(user, Config), 1}, {geralt, 1}], fun(Carol, Geralt) ->
  414:         special_chars_helper:check_attr_from_to(Carol, Geralt),
  415:         special_chars_helper:check_attr_from_to(Geralt, Carol)
  416:     end).
  417: 
  418: cant_send_invalid_rid(Config) ->
  419:     escalus:story(Config, [{?config(user, Config), 1}], fun(Carol) ->
  420:         %% ct:pal("This test will leave invalid rid, session not found"
  421:         %%        " errors in the server log~n"),
  422: 
  423:         %% NOTICE 1
  424:         %% This test will provoke the server to log the following message:
  425:         %%
  426:         %% mod_bosh_socket:handle_stream_event:401
  427:         %% invalid rid XXX, expected YYY, difference ?INVALID_RID_OFFSET:
  428: 
  429:         %% NOTICE 2
  430:         %% Escalus will try to close the session under test when the story
  431:         %% completes. This will leave the following message in the log:
  432:         %%
  433:         %% mod_bosh:forward_body:265 session not found!
  434: 
  435:         %% NOTICE 3
  436:         %% We enable quickfail mode, because sometimes request with invalid RID
  437:         %% arrives before empty body req. with valid RID, so server returns an error
  438:         %% only for the first req. and escalus_bosh in normal mode would get stuck
  439:         %% since it wants to maintain order according to RIDs
  440: 
  441:         escalus_bosh:set_quickfail(Carol, true),
  442: 
  443:         InvalidRid = get_bosh_rid(Carol) + ?INVALID_RID_OFFSET,
  444:         Sid = get_bosh_sid(Carol),
  445:         Empty = escalus_bosh:empty_body(InvalidRid, Sid),
  446:         bosh_send_raw(Carol, Empty),
  447: 
  448:         escalus:assert(is_stream_end, escalus:wait_for_stanza(Carol)),
  449:         escalus_client:wait_for_close(Config, Carol, timer:seconds(1)),
  450:         {ok, false} = wait_for_session_close(Sid)
  451: 
  452:         end).
  453: 
  454: multiple_stanzas(Config) ->
  455:     escalus:story(Config, [{?config(user, Config), 1}, {geralt, 1}, {alice, 1}],
  456:                   fun(Carol, Geralt, Alice) ->
  457:                 %% send a multiple stanza
  458:                 Server = escalus_client:server(Carol),
  459:                 Stanza1 = escalus_stanza:chat_to(Geralt, <<"Hello">>),
  460:                 Stanza2 = escalus_stanza:chat_to(Alice, <<"Hello">>),
  461:                 Stanza3 = escalus_stanza:service_discovery(Server),
  462: 
  463:                 RID = get_bosh_rid(Carol),
  464:                 SID = get_bosh_sid(Carol),
  465:                 Body = escalus_bosh:empty_body(RID, SID),
  466:                 Stanza = Body#xmlel{children =
  467:                                           [Stanza1, Stanza2, Stanza1, Stanza3]},
  468:                 bosh_send_raw(Carol, Stanza),
  469: 
  470:                 %% check whether each of stanzas has been processed correctly
  471:                 escalus:assert(is_chat_message, [<<"Hello">>],
  472:                                escalus_client:wait_for_stanza(Geralt)),
  473:                 escalus:assert(is_chat_message, [<<"Hello">>],
  474:                                escalus_client:wait_for_stanza(Alice)),
  475:                 escalus:assert(is_chat_message, [<<"Hello">>],
  476:                                escalus_client:wait_for_stanza(Geralt)),
  477:                 escalus:assert(is_iq_result, escalus:wait_for_stanza(Carol))
  478:         end).
  479: 
  480: namespace(Config) ->
  481:     escalus:story(Config, [{?config(user, Config), 1}, {geralt, 1}],
  482:         fun(Carol, Geralt) ->
  483:             %% send a multiple stanza
  484:             Server = escalus_client:server(Carol),
  485: 
  486:             Stanza1 = escalus_stanza:service_discovery(Server),
  487:             Stanza2 = escalus_stanza:chat_to(Carol, <<"Hello">>),
  488:             Stanza3 = escalus_stanza:presence_direct(Carol, <<"available">>),
  489: 
  490:             RID = get_bosh_rid(Carol),
  491:             SID = get_bosh_sid(Carol),
  492:             Body = escalus_bosh:empty_body(RID, SID),
  493:             Stanza = Body#xmlel{children=[Stanza1]},
  494:             bosh_send_raw(Carol, Stanza),
  495: 
  496:             IQResp = escalus:wait_for_stanza(Carol),
  497: 
  498:             escalus:assert(is_iq, [<<"result">>], IQResp),
  499:             escalus:assert(has_ns, [<<"jabber:client">>], IQResp),
  500: 
  501:             escalus_client:send(Geralt, Stanza2),
  502:             escalus_client:send(Geralt, Stanza3),
  503: 
  504:             Message = escalus:wait_for_stanza(Carol),
  505:             escalus:assert(is_chat_message, Message),
  506:             escalus:assert(has_ns, [<<"jabber:client">>], Message),
  507: 
  508:             Presence  = escalus:wait_for_stanza(Carol),
  509:             escalus:assert(is_presence, Presence),
  510:             escalus:assert(has_ns, [<<"jabber:client">>], Presence)
  511:         end).
  512: 
  513: disconnect_inactive(Config) ->
  514:     escalus:fresh_story(Config, [{carol, 1}, {geralt, 1}], fun(Carol, Geralt) ->
  515: 
  516:         Sid = get_bosh_sid(Carol),
  517:         {_, Sid, CarolSessionPid} = get_bosh_session(Sid),
  518:         %% Don't send new long-polling requests waiting for server push.
  519:         set_keepalive(Carol, false),
  520: 
  521:         %% Make Carol receive using the last remaining connection.
  522:         escalus_client:send(Geralt,
  523:                             escalus_stanza:chat_to(Carol, <<"Hello!">>)),
  524: 
  525:         escalus:assert(is_chat_message, [<<"Hello!">>],
  526:                        escalus_client:wait_for_stanza(Carol)),
  527: 
  528:         %% Ensure all connections for Carol have been closed.
  529:         [] = get_handlers(CarolSessionPid),
  530: 
  531:         %% Wait for disconnection because of inactivity timeout.
  532:         %% Assert Carol has been disconnected due to inactivity.
  533:         wait_for_session_close(Sid),
  534:         %% We don't need to close the session in escalus_bosh:stop/1
  535:         escalus_client:kill_connection(Config, Carol)
  536: 
  537:         end).
  538: 
  539: connection_interrupted(Config) ->
  540:     escalus:fresh_story(Config, [{carol, 1}], fun(Carol) ->
  541: 
  542:         %% Sanity check - there should be one BOSH session belonging
  543:         %% to Carol.
  544:         %% Turn off Escalus auto-reply, so that inactivity is triggered.
  545:         set_keepalive(Carol, false),
  546: 
  547:         Sid = get_bosh_sid(Carol),
  548: 
  549:         %% Terminate the connection, but don't notify the server.
  550:         escalus_client:kill_connection(Config, Carol),
  551: 
  552:         %% Assert Carol has not been disconnected yet.
  553:         timer:sleep(100),
  554:         true = is_session_alive(Sid),
  555: 
  556:         %% Wait for disconnection because of inactivity timeout.
  557:         %% Keep in mind this only works due to the max_wait also being lowered.
  558:         %% In other words, wait timeout must happen, so that there are
  559:         %% no requests held by the server for inactivity to cause disconnection.
  560:         %% Assert Carol has been disconnected due to inactivity.
  561:         wait_for_session_close(Sid, timer:seconds(?INACTIVITY) + timer:seconds(?MAX_WAIT))
  562:         end).
  563: 
  564: %% Ensure that a new request replacing an existing long-poll does not start the
  565: %% inactivity timer.
  566: interrupt_long_poll_is_activity(ConfigIn) ->
  567:     Config = escalus_users:update_userspec(ConfigIn, carol, bosh_wait, 10),
  568: 
  569:     escalus:fresh_story(Config, [{carol, 1}, {geralt, 1}], fun(Carol, Geralt) ->
  570: 
  571:         %% Sanity check - there should be one BOSH session belonging
  572:         %% to Carol and one handler for Carol.
  573:         Sid = get_bosh_sid(Carol),
  574:         {_, _, CarolSessionPid} = get_bosh_session(Sid),
  575:         {ok, 1} = wait_for_handler(CarolSessionPid, 1),
  576: 
  577:         %% Send a message.  A new connection should be established, and
  578:         %% the existing long-poll connection should be closed.
  579:         escalus_client:send(Carol,
  580:                             escalus_stanza:chat_to(Geralt, <<"Hello!">>)),
  581: 
  582:         %% Wait until after the inactivity timeout (which should be less than
  583:         %% the BOSH wait timeout).
  584:         timer:sleep(2 * timer:seconds(?INACTIVITY)),
  585: 
  586:         %% No disconnection should have occurred.
  587:         escalus_assert:has_no_stanzas(Carol),
  588:         true = is_session_alive(Sid),
  589:         {ok, 1} = wait_for_handler(CarolSessionPid, 1)
  590: 
  591:         end).
  592: 
  593: reply_on_pause(Config) ->
  594:     escalus:fresh_story(Config, [{carol, 1}], fun(Carol) ->
  595: 
  596:         Sid = get_bosh_sid(Carol),
  597:         {_, _, CarolSessionPid} = get_bosh_session(Sid),
  598:         set_keepalive(Carol, false),
  599: 
  600:         %% Sanity check - there should be one handler for Carol.
  601:         {ok, 1} = wait_for_handler(CarolSessionPid, 1),
  602: 
  603:         pause(Carol, 10),
  604: 
  605:         %% There should be no handlers for Carol,
  606:         %% but the session should be alive.
  607:         true = is_session_alive(Sid),
  608:         0 = length(get_handlers(CarolSessionPid)),
  609:         0 = get_bosh_requests(Carol)
  610: 
  611:         end).
  612: 
  613: cant_pause_for_too_long(Config) ->
  614:     escalus:fresh_story(Config, [{carol, 1}], fun(Carol) ->
  615: 
  616:         Sid = get_bosh_sid(Carol),
  617:         {_, _, CarolSessionPid} = get_bosh_session(Sid),
  618:         set_keepalive(Carol, false),
  619: 
  620:         %% Sanity check - there should be one handler for Carol.
  621:         {ok, 1} = wait_for_handler(CarolSessionPid, 1),
  622: 
  623:         pause(Carol, 10000),
  624: 
  625:         escalus:assert(is_stream_end, escalus:wait_for_stanza(Carol)),
  626:         escalus_client:wait_for_close(Config, Carol, timer:seconds(1)),
  627:         false = is_session_alive(Sid)
  628: 
  629:         end).
  630: 
  631: %% Ensure that a pause request causes inactivity timer cancellation.
  632: pause_request_is_activity(Config) ->
  633:     escalus:fresh_story(Config, [{carol, 1}], fun(Carol) ->
  634: 
  635:         Sid = get_bosh_sid(Carol),
  636:         {_, _, CarolSessionPid} = get_bosh_session(Sid),
  637:         set_keepalive(Carol, false),
  638: 
  639:         %% Sanity check - there should be one handler for Carol.
  640:         {ok, 1} = wait_for_handler(CarolSessionPid, 1),
  641: 
  642:         %% Wait most of the allowed inactivity interval.
  643:         timer:sleep(timer:seconds(?INACTIVITY - 1)),
  644: 
  645:         %% This should cancel the inactivity timer.
  646:         pause(Carol, 10),
  647:         timer:sleep(timer:seconds(?INACTIVITY - 1)),
  648:         %% No disconnection should've occured.
  649:         escalus_assert:has_no_stanzas(Carol),
  650:         true = is_session_alive(Sid)
  651: 
  652:         end).
  653: 
  654: reply_in_time(ConfigIn) ->
  655:     Config = escalus_users:update_userspec(ConfigIn, carol, bosh_wait, 1),
  656:     escalus:fresh_story(Config, [{carol, 1}, {geralt, 1}], fun(Carol, Geralt) ->
  657: 
  658:         Wait = proplists:get_value(bosh_wait,
  659:                                    escalus_users:get_userspec(Config, carol)),
  660: 
  661:         %% Don't send new long-polling requests waiting for server push.
  662:         set_keepalive(Carol, false),
  663: 
  664:         %% Make Carol receive using the last remaining connection.
  665:         escalus_client:send(Geralt,
  666:                             escalus_stanza:chat_to(Carol, <<"Hello!">>)),
  667:         escalus:assert(is_chat_message, [<<"Hello!">>],
  668:                        escalus_client:wait_for_stanza(Carol)),
  669: 
  670:         %% Sanity check - there should be no awaiting handlers.
  671:         {_, _, CarolSessionPid} = get_bosh_session(get_bosh_sid(Carol)),
  672:         0 = length(get_handlers(CarolSessionPid)),
  673: 
  674:         %% Send a single request and assert it's registered by server.
  675:         Rid = get_bosh_rid(Carol),
  676:         Sid = get_bosh_sid(Carol),
  677:         Empty = escalus_bosh:empty_body(Rid, Sid),
  678:         bosh_send_raw(Carol, Empty),
  679:         timer:sleep(100),
  680:         {ok, 1} = wait_for_handler(CarolSessionPid, 1),
  681: 
  682:         %% Assert the server has responded to that request.
  683:         wait_for_handler(CarolSessionPid, 0, timer:seconds(Wait) + 100)
  684:         end).
  685: 
  686: server_acks(Config) ->
  687:     escalus:story(Config, [{carol, 1}, {geralt, 1}], fun(Carol, Geralt) ->
  688:         bosh_set_active(Carol, false),
  689:         ExpectedRid = list_to_binary(integer_to_list(get_bosh_rid(Carol))),
  690:         escalus_client:send(Carol, escalus_stanza:chat_to(Geralt, <<"1st!">>)),
  691:         escalus_client:send(Carol, escalus_stanza:chat_to(Geralt, <<"2nd!">>)),
  692:         timer:sleep(200),
  693: 
  694:         All = recv_all(Carol),
  695:         ExpectedRid = exml_query:attr(hd(All), <<"ack">>),
  696:         bosh_set_active(Carol, true)
  697: 
  698:         end).
  699: 
  700: force_report(Config) ->
  701: 
  702:     %% Carol stores current Rid1
  703:     %% Carol sends msg1
  704:     %% Carol sends msg2
  705:     %% Geralt sends a reply to msg1
  706:     %% Geralt sends a reply to msg2
  707:     %% Carol recvs a reply to msg1
  708:     %% Carol recvs a reply to msg2
  709:     %% Carol sends an ack with Rid1 on empty BOSH wrapper
  710:     %% server sends a report
  711: 
  712:     escalus:story(Config, [{carol, 1}, {geralt, 1}], fun(Carol, Geralt) ->
  713: 
  714:         StaleRid = get_bosh_rid(Carol),
  715:         escalus_client:send(Carol, chat_to(Geralt, <<"1st msg">>)),
  716:         escalus_client:send(Carol, chat_to(Geralt, <<"2nd msg">>)),
  717:         wait_for_stanzas(Geralt, 2),
  718:         escalus_client:send(Geralt, chat_to(Carol, <<"1st rep">>)),
  719:         escalus_client:send(Geralt, chat_to(Carol, <<"2nd rep">>)),
  720:         escalus:assert(is_chat_message, [<<"1st rep">>],
  721:                        wait_for_stanza(Carol)),
  722:         escalus:assert(is_chat_message, [<<"2nd rep">>],
  723:                        wait_for_stanza(Carol)),
  724: 
  725:         %% Turn on client acknowledgement checking for Carol
  726:         {_, _, CarolSessionPid} = get_bosh_session(get_bosh_sid(Carol)),
  727:         set_client_acks(CarolSessionPid, true),
  728: 
  729:         bosh_set_active(Carol, false),
  730:         %% Send ack with StaleRid
  731:         Rid = get_bosh_rid(Carol),
  732:         Sid = get_bosh_sid(Carol),
  733:         BodyWithAck = ack_body(escalus_bosh:empty_body(Rid, Sid), StaleRid),
  734:         bosh_send_raw(Carol, BodyWithAck),
  735: 
  736:         %% Turn off client acknowledgement checking - don't cause server error
  737:         %% on subsequent requests without 'ack' attribute.
  738:         set_client_acks(CarolSessionPid, false),
  739: 
  740:         %% Expect a server report
  741:         timer:sleep(100),
  742:         MaybeReport = bosh_recv(Carol),
  743:         escalus:assert(is_bosh_report, [StaleRid+1], MaybeReport),
  744:         bosh_set_active(Carol, true)
  745: 
  746:         end).
  747: 
  748: force_retransmission(Config) ->
  749:     escalus:story(Config, [{carol, 1}, {geralt, 1}], fun(Carol, Geralt) ->
  750: 
  751:         %% `send_raw` must be used as the exact request structure
  752:         %% is needed later for retransmission.
  753:         %% Hence, construct the request manually.
  754:         Rid = get_bosh_rid(Carol),
  755:         Sid = get_bosh_sid(Carol),
  756:         Empty = escalus_bosh:empty_body(Rid, Sid),
  757:         Chat = Empty#xmlel{
  758:                 children = [escalus_stanza:chat_to(Geralt, <<"1st msg!">>)]},
  759: 
  760:         %% Send msg, recv msg, send reply, recv reply.
  761:         %% This synchronous sequence sets up the server
  762:         %% to have the reply for Chat cached.
  763:         bosh_send_raw(Carol, Chat),
  764:         escalus:assert(is_chat_message, [<<"1st msg!">>],
  765:                        wait_for_stanza(Geralt)),
  766:         escalus_client:send(Geralt, chat_to(Carol, <<"1st rep!">>)),
  767:         ChatResponse = wait_for_stanza(Carol),
  768:         escalus:assert(is_chat_message, [<<"1st rep!">>], ChatResponse),
  769: 
  770:         %% Resend msg.
  771:         bosh_resend_raw(Carol, Chat),
  772: 
  773:         %% Recv same reply again.
  774:         ChatResponse = wait_for_stanza(Carol)
  775: 
  776:         end).
  777: 
  778: force_cache_trimming(Config) ->
  779:     escalus:story(Config, [{carol, 1}, {geralt, 1}], fun(Carol, Geralt) ->
  780: 
  781:         Sid = get_bosh_sid(Carol),
  782: 
  783:         {_, _, CarolSessionPid} = get_bosh_session(Sid),
  784:         set_client_acks(CarolSessionPid, true),
  785: 
  786:         %% Ack now
  787:         Rid1 = get_bosh_rid(Carol),
  788:         Ack1 = ack_body(escalus_bosh:empty_body(Rid1, Sid), Rid1-1),
  789:         bosh_send_raw(Carol, Ack1),
  790: 
  791:         %% Exchange 2 messages
  792:         Rid2 = get_bosh_rid(Carol),
  793:         Chat = (escalus_bosh:empty_body(Rid2, Sid))#xmlel{
  794:                 children = [escalus_stanza:chat_to(Geralt, <<"1st msg!">>)]},
  795:         bosh_send_raw(Carol, Chat),
  796:         escalus:assert(is_chat_message, [<<"1st msg!">>],
  797:                        wait_for_stanza(Geralt)),
  798:         escalus_client:send(Geralt, chat_to(Carol, <<"1st rep!">>)),
  799:         ChatResponse = wait_for_stanza(Carol),
  800:         escalus:assert(is_chat_message, [<<"1st rep!">>], ChatResponse),
  801: 
  802:         %% Ack/Chat again
  803:         Rid3 = get_bosh_rid(Carol),
  804:         AckedChat = (ack_body(escalus_bosh:empty_body(Rid3, Sid), Rid2))#xmlel{
  805:                 children = [escalus_stanza:chat_to(Geralt, <<"2nd msg!">>)]},
  806:         bosh_send_raw(Carol, AckedChat),
  807:         escalus:assert(is_chat_message, [<<"2nd msg!">>],
  808:                        wait_for_stanza(Geralt)),
  809: 
  810:         %% The cache should now contain only entries newer than Rid2.
  811:         {_, _, CarolSessionPid} = get_bosh_session(Sid),
  812:         Cache = get_cached_responses(CarolSessionPid),
  813:         true = lists:all(fun({R, _, _}) when R > Rid2 -> true; (_) -> false end,
  814:                          Cache),
  815:         %% Not sure about this one...
  816:         1 = length(Cache),
  817: 
  818:         set_client_acks(CarolSessionPid, false)
  819: 
  820:         end).
  821: 
  822: %%--------------------------------------------------------------------
  823: %% Helpers
  824: %%--------------------------------------------------------------------
  825: 
  826: get_bosh_sessions() ->
  827:     rpc(mim(), mod_bosh_backend, get_sessions, []).
  828: 
  829: get_bosh_session(Sid) ->
  830:     BoshSessions = get_bosh_sessions(),
  831:     lists:keyfind(Sid, 2, BoshSessions).
  832: 
  833: get_handlers(BoshSessionPid) ->
  834:     rpc(mim(), mod_bosh_socket, get_handlers, [BoshSessionPid]).
  835: 
  836: get_bosh_sid(#client{rcv_pid = Pid}) ->
  837:     escalus_bosh:get_sid(Pid).
  838: 
  839: get_bosh_rid(#client{rcv_pid = Pid}) ->
  840:     escalus_bosh:get_rid(Pid).
  841: 
  842: set_keepalive(#client{rcv_pid = Pid}, Keepalive) ->
  843:     escalus_bosh:set_keepalive(Pid, Keepalive).
  844: 
  845: mark_as_terminated(#client{rcv_pid = Pid}) ->
  846:     escalus_bosh:mark_as_terminated(Pid).
  847: 
  848: pause(#client{rcv_pid = Pid}, Seconds) ->
  849:     escalus_bosh:pause(Pid, Seconds),
  850:     timer:sleep(100).
  851: 
  852: start_client(Config, User, Res) ->
  853:     NamedSpecs = escalus_config:get_config(escalus_users, Config),
  854:     UserSpec = [{keepalive, false} | proplists:get_value(User, NamedSpecs)],
  855:     {ok, Client} = escalus_client:start(Config, UserSpec, Res),
  856:     Client.
  857: 
  858: bosh_set_active(#client{rcv_pid = Pid}, Value) ->
  859:     escalus_bosh:set_active(Pid, Value).
  860: 
  861: is_bosh_connected(#client{rcv_pid = Pid}) ->
  862:     escalus_bosh:is_connected(Pid).
  863: 
  864: get_bosh_requests(#client{rcv_pid = Pid}) ->
  865:     escalus_bosh:get_requests(Pid).
  866: 
  867: recv_all(Client) ->
  868:     recv_all(bosh_recv(Client), Client, []).
  869: 
  870: recv_all(empty, _Client, Acc) ->
  871:     lists:reverse(Acc);
  872: recv_all(Element, Client, Acc) ->
  873:     recv_all(bosh_recv(Client), Client, [Element | Acc]).
  874: 
  875: bosh_recv(#client{rcv_pid = Pid}) ->
  876:     escalus_bosh:recv(Pid).
  877: 
  878: bosh_send_raw(#client{rcv_pid = Pid}, Body) ->
  879:     escalus_bosh:send_raw(Pid, Body).
  880: 
  881: bosh_resend_raw(#client{rcv_pid = Pid}, Body) ->
  882:     escalus_bosh:resend_raw(Pid, Body).
  883: 
  884: chat_to(Client, Content) ->
  885:     escalus_stanza:chat_to(Client, Content).
  886: 
  887: wait_for_stanzas(Client, Count) ->
  888:     escalus_client:wait_for_stanzas(Client, Count).
  889: 
  890: wait_for_stanza(Client) ->
  891:     escalus_client:wait_for_stanza(Client).
  892: 
  893: ack_body(Body, Rid) ->
  894:     Attrs = Body#xmlel.attrs,
  895:     Ack = {<<"ack">>, list_to_binary(integer_to_list(Rid))},
  896:     NewAttrs = lists:keystore(<<"ack">>, 1, Attrs, Ack),
  897:     Body#xmlel{attrs = NewAttrs}.
  898: 
  899: set_client_acks(SessionPid, Enabled) ->
  900:     rpc(mim(), mod_bosh_socket, set_client_acks, [SessionPid, Enabled]).
  901: 
  902: get_cached_responses(SessionPid) ->
  903:     rpc(mim(), mod_bosh_socket, get_cached_responses, [SessionPid]).
  904: 
  905: is_session_alive(Sid) ->
  906:     BoshSessions = get_bosh_sessions(),
  907:     lists:keymember(Sid, 2, BoshSessions).
  908: 
  909: wait_for_session_close(Sid) ->
  910:     wait_for_session_close(Sid, ?INACTIVITY).
  911: 
  912: wait_for_session_close(Sid, LeftTime) ->
  913:     mongoose_helper:wait_until(fun() -> is_session_alive(Sid) end, false,
  914:                                #{
  915:                                  time_left => timer:seconds(10),
  916:                                  time_sleep => LeftTime,
  917:                                  name => is_session_alive
  918:                                 }).
  919: 
  920: wait_for_handler(Pid, Count) ->
  921:     mongoose_helper:wait_until(fun() -> length(get_handlers(Pid)) end, Count,
  922:                                 #{
  923:                                  time_left => timer:seconds(10),
  924:                                  time_sleep => timer:seconds(1),
  925:                                  name => get_handlers
  926:                                 }).
  927: 
  928: 
  929: wait_for_handler(Pid, Count, LeftTime) ->
  930:     mongoose_helper:wait_until(fun() -> length(get_handlers(Pid)) end, Count,
  931:                                #{
  932:                                  time_left => LeftTime,
  933:                                  time_sleep => timer:seconds(1),
  934:                                  name => get_handlers
  935:                                 }).
  936: 
  937: wait_until_user_has_no_stanzas(User) ->
  938:         mongoose_helper:wait_until(fun() ->
  939:                                        escalus_assert:has_no_stanzas(User)
  940:                                    end, ok, #{left_time => 2 * timer:seconds(?INACTIVITY)}).
  941: 
  942: wait_for_zero_bosh_sessions() ->
  943:     mongoose_helper:wait_until(fun() ->
  944:                                        length(get_bosh_sessions())
  945:                                end,
  946:                                0,
  947:                                #{name => get_bosh_sessions}).