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