1: -module(jingle_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("common_test/include/ct.hrl").
    5: -include_lib("exml/include/exml.hrl").
    6: -include_lib("eunit/include/eunit.hrl").
    7: 
    8: -import(distributed_helper, [mim/0, mim2/0,
    9:                              require_rpc_nodes/1,
   10:                              rpc/4]).
   11: 
   12: -import(jingle_helper, [content/1,
   13:                         content_group/1]).
   14: 
   15: -import(domain_helper, [domain/0]).
   16: 
   17: %%--------------------------------------------------------------------
   18: %% Suite configuration
   19: %%--------------------------------------------------------------------
   20: 
   21: all() ->
   22:     [{group, all}].
   23: 
   24: groups() ->
   25:     G = [{all, [parallel], test_cases()}],
   26:     ct_helper:repeat_all_until_all_ok(G).
   27: 
   28: test_cases() ->
   29:     [jingle_session_is_established_for_full_jids,
   30:      jingle_session_is_established_for_full_jids_on_different_nodes,
   31:      resp_4xx_from_sip_proxy_results_in_session_terminate,
   32:      jingle_session_is_established_when_calling_a_number,
   33:      jingle_session_is_established_and_terminated_by_initiator,
   34:      jingle_session_is_established_and_terminated_by_receiver,
   35:      jingle_session_is_established_and_terminated_by_receiver_on_different_node,
   36:      jingle_session_is_intiated_and_canceled_by_initiator,
   37:      jingle_session_is_intiated_and_canceled_by_receiver,
   38:      jingle_session_is_intiated_and_canceled_by_receiver_on_different_node,
   39:      jingle_session_is_established_with_a_conference_room,
   40:      jingle_session_is_terminated_on_other_receivers_devices,
   41:      jingle_session_initiate_is_resent_on_demand,
   42:      mongoose_replies_with_480_when_invitee_is_offline,
   43:      mongoose_returns_404_when_not_authorized_user_tires_to_accept_a_session,
   44:      mongoose_returns_404_when_nto_authorized_user_tries_to_cancel_a_session,
   45:      mongoose_sends_reINVITE_on_source_remove_action,
   46:      mongoose_sends_reINVITE_on_source_add_action,
   47:      mongoose_sends_reINVITE_on_source_update_action
   48: 
   49:     ].
   50: 
   51: suite() ->
   52:     require_rpc_nodes([mim]) ++ escalus:suite().
   53: 
   54: %%--------------------------------------------------------------------
   55: %% Init & teardown
   56: %%--------------------------------------------------------------------
   57: 
   58: init_per_suite(Config) ->
   59:     case rpc(mim(), application, get_application, [nksip]) of
   60:         {ok, nksip} ->
   61:             distributed_helper:add_node_to_cluster(mim2(), Config),
   62:             start_nksip_in_mim_nodes(),
   63:             application:ensure_all_started(esip),
   64:             spawn(fun() -> ets:new(jingle_sip_translator, [public, named_table]),
   65:                            ets:new(jingle_sip_translator_bindings, [public, named_table]),
   66:                            receive stop -> ok end end),
   67:             esip:add_listener(12345, tcp, []),
   68:             esip:set_config_value(module, jingle_sip_translator),
   69:             escalus:init_per_suite(Config);
   70:         undefined ->
   71:             {skip, build_was_not_configured_with_jingle_sip}
   72:     end.
   73: 
   74: start_nksip_in_mim_nodes() ->
   75:     Pid1 = start_nskip_in_parallel(mim(), #{}),
   76:     Pid2 = start_nskip_in_parallel(mim2(), #{listen_port => 12346}),
   77:     wait_for_process_to_stop(Pid1),
   78:     wait_for_process_to_stop(Pid2).
   79: 
   80: wait_for_process_to_stop(Pid) ->
   81:     erlang:monitor(process, Pid),
   82:     receive
   83:         {'DOWN', _, process, Pid, _} -> ok
   84:     after timer:seconds(60) ->
   85:               ct:fail(wait_for_process_to_stop_timeout)
   86:     end.
   87: 
   88: start_nskip_in_parallel(NodeSpec, ExtraOpts) ->
   89:     Domain = domain(),
   90:     Opts = #{proxy_host => <<"localhost">>,
   91:              proxy_port => 12345,
   92:              backend => ct_helper:get_internal_database()},
   93:     OptsWithExtra = maps:merge(Opts, ExtraOpts),
   94:     AllOpts = config_parser_helper:mod_config(mod_jingle_sip, OptsWithExtra),
   95:     RPCSpec = NodeSpec#{timeout => timer:seconds(60)},
   96:     proc_lib:spawn_link(dynamic_modules, start, [RPCSpec, Domain, mod_jingle_sip, AllOpts]).
   97: 
   98: end_per_suite(Config) ->
   99:     escalus_fresh:clean(),
  100:     Domain = domain(),
  101:     dynamic_modules:stop(mim(), Domain, mod_jingle_sip),
  102:     dynamic_modules:stop(mim2(), Domain, mod_jingle_sip),
  103:     distributed_helper:remove_node_from_cluster(mim2(), Config),
  104:     escalus:end_per_suite(Config).
  105: 
  106: init_per_group(_GroupName, Config) ->
  107:     Config.
  108: 
  109: end_per_group(_GroupName, Config) ->
  110:     Config.
  111: 
  112: init_per_testcase(CaseName, Config) ->
  113:     escalus:init_per_testcase(CaseName, Config).
  114: 
  115: end_per_testcase(CaseName, Config) ->
  116:     escalus:end_per_testcase(CaseName, Config).
  117: 
  118: %%--------------------------------------------------------------------
  119: %% Cases
  120: %%--------------------------------------------------------------------
  121: 
  122: jingle_session_is_established_for_full_jids(Config) ->
  123:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  124:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  125: 
  126:         accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest)
  127:     end).
  128: 
  129: jingle_session_is_established_for_full_jids_on_different_nodes(Config) ->
  130:     escalus:fresh_story(Config, [{alice, 1}, {clusterguy, 1}], fun(Alice, Bob) ->
  131:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  132: 
  133:         accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest)
  134:     end).
  135: 
  136: resp_4xx_from_sip_proxy_results_in_session_terminate(Config) ->
  137:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  138:         {_InviteStanza, FirstIQSet} = send_initiate_and_wait_for_first_iq_set(Alice, <<"error.480@localhost">>),
  139:         assert_is_session_terminate(FirstIQSet, <<"general-error">>)
  140: 
  141:     end).
  142: 
  143: jingle_session_is_established_when_calling_a_number(Config) ->
  144:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  145:         {InviteStanza, FirstIQSet} = send_initiate_and_wait_for_first_iq_set(Alice, <<"+488790@numbers.localhost">>),
  146:         assert_ringing(InviteStanza, FirstIQSet),
  147:         AcceptInfo = escalus:wait_for_stanza(Alice, timer:seconds(5)),
  148:         assert_accept_response(InviteStanza, AcceptInfo),
  149: 
  150:         ok
  151: 
  152:     end).
  153: 
  154: jingle_session_is_established_with_a_conference_room(Config) ->
  155:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  156:         {InviteStanza, FirstIQSet} = send_initiate_and_wait_for_first_iq_set(Alice, <<"*901@numbers.localhost">>),
  157:         assert_ringing(InviteStanza, FirstIQSet),
  158:         AcceptInfo = escalus:wait_for_stanza(Alice, timer:seconds(5)),
  159:         assert_accept_response(InviteStanza, AcceptInfo),
  160: 
  161:         TransportInfo = escalus:wait_for_stanza(Alice),
  162:         assert_transport_info(InviteStanza, TransportInfo),
  163: 
  164:         ok
  165:     end).
  166: 
  167: jingle_session_initiate_is_resent_on_demand(Config) ->
  168:     escalus:fresh_story(Config, [{alice, 1}, {bob, 2}], fun(Alice, Bob, Bob2) ->
  169:         %% Bob2 becomes unavailalbe
  170:         push_helper:become_unavailable(Bob2),
  171:         escalus:wait_for_stanza(Bob), %%Bob first device gets unavailable form the other
  172:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  173: 
  174:         escalus_assert:has_no_stanzas(Bob2),
  175: 
  176:         SID = exml_query:path(InviteRequest, path_to_jingle_sid()),
  177: 
  178:         ResendSessionInitiateIQ = iq_get(jingle_element(SID, <<"existing-session-initiate">>, [])),
  179: 
  180:         %% Bob2 becomes available
  181:         escalus:send(Bob2, escalus_stanza:presence(<<"available">>)),
  182:         escalus:wait_for_stanzas(Bob2, 2), %% 2 presence stansa from Bob2 and Bob
  183:         %% and asks for the session-initiate received by the other device
  184:         %% this is to get the invite in new session (new browser window)
  185:         escalus:send(Bob2, ResendSessionInitiateIQ),
  186:         ResendResult = escalus:wait_for_stanza(Bob2),
  187:         escalus:assert(is_iq_result, [ResendSessionInitiateIQ], ResendResult),
  188:         InviteRequest2 = escalus:wait_for_stanza(Bob2),
  189:         assert_same_sid(InviteRequest, InviteRequest2),
  190:         assert_invite_request(InviteStanza, InviteRequest2),
  191:         AliceShortJID = escalus_client:short_jid(Alice),
  192:         escalus:assert(is_stanza_from, [AliceShortJID], InviteRequest2)
  193: 
  194: 
  195:     end).
  196: 
  197: jingle_session_is_terminated_on_other_receivers_devices(Config) ->
  198:     escalus:fresh_story(Config, [{alice, 1}, {bob, 2}], fun(Alice, Bob, Bob2) ->
  199:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  200:         %% The other Bob's device also gets the invite
  201:         InviteRequest2 = escalus:wait_for_stanza(Bob2),
  202: 
  203:         %% then bob accepts the call on one of the devices
  204:         assert_same_sid(InviteRequest, InviteRequest2),
  205:         accept_jingle_session(Alice, Bob2, InviteStanza, InviteRequest2),
  206: 
  207:         %% then Bob's first device gets cancel request
  208:         Terminate = escalus:wait_for_stanza(Bob),
  209:         assert_is_session_terminate(Terminate, <<"cancel">>)
  210: 
  211:     end).
  212: 
  213: 
  214: mongoose_replies_with_480_when_invitee_is_offline(Config) ->
  215:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  216:         %% Bob becomes unavailalbe
  217:         push_helper:become_unavailable(Bob),
  218:         jingle_sip_translator:send_invite(Alice, Bob, self()),
  219: 
  220:         receive
  221:             {sip_resp, 480} ->
  222:                 ok;
  223:             {sip_resp, Other} ->
  224:                 ct:fail("Received SIP resp: ~p", [Other])
  225:         after timer:seconds(5) ->
  226:                   ct:fail(timeout_waiting_for_sip_resp)
  227:         end
  228: 
  229:     end).
  230: 
  231: mongoose_returns_404_when_not_authorized_user_tires_to_accept_a_session(Config) ->
  232:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  233:         {InviteStanza, _InviteRequest} = initiate_jingle_session(Alice, Bob),
  234:         %% Then Kate tries to accept Alice's session
  235:         AcceptStanza = escalus_stanza:to(jingle_accept(InviteStanza), Bob),
  236:         escalus:send(Kate, AcceptStanza),
  237:         AcceptResult = escalus:wait_for_stanza(Kate, timer:seconds(5)),
  238:         escalus:assert(is_iq_error, AcceptResult),
  239:         escalus:assert(is_error, [<<"cancel">>, <<"item-not-found">>], AcceptResult)
  240: 
  241: 
  242:     end).
  243: 
  244: mongoose_returns_404_when_nto_authorized_user_tries_to_cancel_a_session(Config) ->
  245:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) ->
  246:         {_InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  247:         %% Then Kate tries to cancel Bob's session
  248: 
  249:         Result = send_session_terminate_request(Kate, Alice, InviteRequest, <<"decline">>),
  250: 
  251:         escalus:assert(is_iq_error, Result),
  252:         escalus:assert(is_error, [<<"cancel">>, <<"item-not-found">>], Result)
  253: 
  254:     end).
  255: 
  256: jingle_session_is_established_and_terminated_by_initiator(Config) ->
  257:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  258:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  259: 
  260:         accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest),
  261:         timer:sleep(1000),
  262: 
  263:         %% Then Alice (who initiated) terminates the call
  264:         terminate_jingle_session(Alice, Bob, InviteStanza)
  265: 
  266:     end).
  267: 
  268: jingle_session_is_established_and_terminated_by_receiver(Config) ->
  269:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  270:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  271: 
  272:         accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest),
  273: 
  274:         timer:sleep(timer:seconds(5)),
  275:         %% then Bob (who was invited to the call) terminates the call
  276:         %% it's important that bob terminates the call from the invite he got
  277:         terminate_jingle_session(Bob, Alice, InviteRequest)
  278: 
  279:     end).
  280: 
  281: jingle_session_is_established_and_terminated_by_receiver_on_different_node(Config) ->
  282:     escalus:fresh_story(Config, [{alice, 1}, {clusterguy, 1}], fun(Alice, Bob) ->
  283:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  284: 
  285:         accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest),
  286: 
  287:         timer:sleep(timer:seconds(5)),
  288:         %% then Bob (who was invited to the call) terminates the call
  289:         %% it's important that bob terminates the call from the invite he got
  290:         terminate_jingle_session(Bob, Alice, InviteRequest)
  291: 
  292:     end).
  293: 
  294: 
  295: jingle_session_is_intiated_and_canceled_by_initiator(Config) ->
  296:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  297:         {InviteStanza, _InviteRequest} = initiate_jingle_session(Alice, Bob),
  298:         timer:sleep(1000),
  299:         %% then Bob (who was invited to the call) terminates the call
  300:         terminate_jingle_session(Alice, Bob, InviteStanza, <<"decline">>)
  301: 
  302:     end).
  303: 
  304: jingle_session_is_intiated_and_canceled_by_receiver(Config) ->
  305:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  306:         {_InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  307: 
  308:         timer:sleep(1000),
  309:         %% then Bob (who was invited to the call) terminates the call
  310:         terminate_jingle_session(Bob, Alice, InviteRequest, <<"decline">>)
  311:     end).
  312: 
  313: jingle_session_is_intiated_and_canceled_by_receiver_on_different_node(Config) ->
  314:     escalus:fresh_story(Config, [{alice, 1}, {clusterguy, 1}], fun(Alice, Bob) ->
  315:         {_InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  316: 
  317:         timer:sleep(1000),
  318:         %% then Bob (who was invited to the call) terminates the call
  319:         terminate_jingle_session(Bob, Alice, InviteRequest, <<"decline">>)
  320:     end).
  321: 
  322: mongoose_sends_reINVITE_on_source_remove_action(Config) ->
  323:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  324:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  325:         accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest),
  326: 
  327:         %% then Alice sends source-remove
  328:         SourceRemoveStanza = escalus_stanza:to(jingle_source_remove(InviteStanza), Bob),
  329:         escalus:send(Alice, SourceRemoveStanza),
  330:         SourceRemoveResult = escalus:wait_for_stanza(Alice),
  331:         escalus:assert(is_iq_result, [SourceRemoveStanza], SourceRemoveResult),
  332:         SourceRemoveToBob = escalus:wait_for_stanza(Bob),
  333:         assert_source_remove_action(SourceRemoveToBob, InviteRequest)
  334:     end).
  335: 
  336: mongoose_sends_reINVITE_on_source_add_action(Config) ->
  337:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  338:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  339:         accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest),
  340: 
  341:         %% then Alice sends source-remove
  342:         SourceRemoveStanza = escalus_stanza:to(jingle_source_add(InviteStanza), Bob),
  343:         escalus:send(Alice, SourceRemoveStanza),
  344:         SourceRemoveResult = escalus:wait_for_stanza(Alice),
  345:         escalus:assert(is_iq_result, [SourceRemoveStanza], SourceRemoveResult),
  346:         SourceRemoveToBob = escalus:wait_for_stanza(Bob),
  347:         assert_source_add_action(SourceRemoveToBob, InviteRequest)
  348:     end).
  349: 
  350: mongoose_sends_reINVITE_on_source_update_action(Config) ->
  351:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  352:         {InviteStanza, InviteRequest} = initiate_jingle_session(Alice, Bob),
  353:         accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest),
  354: 
  355:         %% then Alice sends source-remove
  356:         SourceRemoveStanza = escalus_stanza:to(jingle_source_update(InviteStanza), Bob),
  357:         escalus:send(Alice, SourceRemoveStanza),
  358:         SourceRemoveResult = escalus:wait_for_stanza(Alice),
  359:         escalus:assert(is_iq_result, [SourceRemoveStanza], SourceRemoveResult),
  360:         SourceRemoveToBob = escalus:wait_for_stanza(Bob),
  361:         assert_source_update_action(SourceRemoveToBob, InviteRequest)
  362:     end).
  363: %%--------------------------------------------------------------------
  364: %% Helpers
  365: %%--------------------------------------------------------------------
  366: 
  367: accept_jingle_session(Alice, Bob, InviteStanza, InviteRequest) ->
  368:     AcceptStanza = escalus_stanza:to(jingle_accept(InviteRequest), Alice),
  369:     escalus:send(Bob, AcceptStanza),
  370:     AcceptResult = escalus:wait_for_stanza(Bob, timer:seconds(5)),
  371:     escalus:assert(is_iq_result, AcceptResult),
  372: 
  373:     AcceptInfo = escalus:wait_for_stanza(Alice, timer:seconds(5)),
  374:     assert_accept_response(InviteStanza, AcceptInfo).
  375: 
  376: initiate_jingle_session(Alice, Bob) ->
  377:     {InviteStanza, _FirstIQSet} = send_initiate_and_wait_for_first_iq_set(Alice, Bob),
  378: 
  379:     %jingle_sip_translator:send_invite(Alice, Bob),
  380: 
  381:     InviteRequest = escalus:wait_for_stanza(Bob, timer:seconds(5)),
  382:     escalus:assert(is_iq_set, InviteRequest),
  383:     assert_invite_request(InviteStanza, InviteRequest),
  384:     {InviteStanza, InviteRequest}.
  385: 
  386: send_initiate_and_wait_for_first_iq_set(Alice, Bob) ->
  387:     InviteStanza = escalus_stanza:to(jingle_initiate(), Bob),
  388:     escalus:send(Alice, InviteStanza),
  389:     SessionInitiateResult = escalus:wait_for_stanza(Alice, timer:seconds(5)),
  390:     escalus:assert(is_iq_result, SessionInitiateResult),
  391:     RingingStanza = escalus:wait_for_stanza(Alice, timer:seconds(5)),
  392:     escalus:assert(is_iq_set, RingingStanza),
  393:     {InviteStanza, RingingStanza}.
  394: 
  395: terminate_jingle_session(Terminator, Other, InviteStanza) ->
  396:     terminate_jingle_session(Terminator, Other, InviteStanza, <<"success">>).
  397: 
  398: terminate_jingle_session(Terminator, Other, InviteStanza, Reason) ->
  399:     TerminateResult = send_session_terminate_request(Terminator, Other,
  400:                                                      InviteStanza, Reason),
  401:     escalus:assert(is_iq_result, TerminateResult),
  402: 
  403:     TerminateInfo = escalus:wait_for_stanza(Other, timer:seconds(5)),
  404:     assert_is_session_terminate(TerminateInfo, Reason).
  405: 
  406: send_session_terminate_request(Terminator, Other, InviteStanza, Reason) ->
  407:     TerminateStanza = escalus_stanza:to(jingle_terminate(InviteStanza, Reason), Other),
  408:     escalus:send(Terminator, TerminateStanza),
  409:     escalus:wait_for_stanza(Terminator, timer:seconds(5)).
  410: 
  411: assert_invite_request(InviteStanza, InviteRequest) ->
  412:     JingleEl = exml_query:subelement(InviteRequest, <<"jingle">>),
  413:     ?assertEqual(<<"session-initiate">>,
  414:                  (exml_query:attr(JingleEl, <<"action">>))),
  415: 
  416:     assert_different_sid(InviteStanza, InviteRequest),
  417:     assert_session_description(JingleEl).
  418: 
  419: assert_ringing(InviteStanza, RingingStanza) ->
  420:     JingleEl = exml_query:subelement(RingingStanza, <<"jingle">>),
  421:     ?assertEqual(<<"session-info">>,
  422:                  (exml_query:attr(JingleEl, <<"action">>))),
  423:     Ringing = exml_query:subelement(JingleEl, <<"ringing">>),
  424:     ?assertMatch(#xmlel{}, Ringing),
  425:     ?assertEqual(<<"urn:xmpp:jingle:apps:rtp:info:1">>, exml_query:attr(Ringing, <<"xmlns">>)),
  426: 
  427:     assert_same_sid(InviteStanza, RingingStanza).
  428: 
  429: 
  430: assert_session_description(JingleEl) ->
  431:     Contents = exml_query:subelements(JingleEl, <<"content">>),
  432:     ?assertMatch([#xmlel{} | _], Contents),
  433: 
  434:     [assert_transport_with_candidate(Content) || Content <- Contents],
  435: 
  436:     case Contents of
  437:         [_, _ | _ ] -> %% For at least 2 contents
  438:             ?assertMatch((#xmlel{}), (exml_query:subelement(JingleEl, <<"group">>)));
  439:         _ ->
  440:             ok
  441:     end.
  442: 
  443: assert_transport_with_candidate(Content) ->
  444:     TransportEl = exml_query:subelement(Content, <<"transport">>),
  445:     ?assertMatch((#xmlel{}), TransportEl),
  446:     ?assertMatch([#xmlel{} | _], (exml_query:subelements(TransportEl, <<"candidate">>))).
  447: 
  448: assert_different_sid(Sent, Received) ->
  449:     SIDPath = path_to_jingle_sid(),
  450:     ?assertNotEqual((exml_query:path(Sent, SIDPath)),
  451:                     (exml_query:path(Received, SIDPath))).
  452: assert_same_sid(Sent, Received) ->
  453:     SIDPath = path_to_jingle_sid(),
  454:     ?assertEqual((exml_query:path(Sent, SIDPath)),
  455:                  (exml_query:path(Received, SIDPath))).
  456: 
  457: path_to_jingle_sid() -> [{element, <<"jingle">>}, {attr, <<"sid">>}].
  458: 
  459: assert_accept_response(InviteStanza, AcceptResponse) ->
  460:     JingleEl = exml_query:subelement(AcceptResponse, <<"jingle">>),
  461:     ?assertEqual(<<"session-accept">>,
  462:                  (exml_query:attr(JingleEl, <<"action">>))),
  463: 
  464:     assert_same_sid(InviteStanza, AcceptResponse),
  465:     assert_session_description(JingleEl).
  466: 
  467: assert_is_session_terminate(FirstIQSet, ReasonName) ->
  468:     JingleEl = exml_query:subelement(FirstIQSet, <<"jingle">>),
  469:     ?assertEqual(<<"session-terminate">>,
  470:                  (exml_query:attr(JingleEl, <<"action">>))),
  471:     ?assertMatch((#xmlel{}),
  472:                  exml_query:path(JingleEl,
  473:                                  [{element, <<"reason">>}, {element, ReasonName}])).
  474: 
  475: assert_transport_info(InviteStanza, TransportInfo) ->
  476:     JingleEl = exml_query:subelement(TransportInfo, <<"jingle">>),
  477:     ?assertEqual(<<"transport-info">>,
  478:                  (exml_query:attr(JingleEl, <<"action">>))),
  479:     assert_same_sid(InviteStanza, TransportInfo).
  480: 
  481: assert_source_remove_action(SourceRemoveRequest, InviteRequest) ->
  482:     assert_same_sid(InviteRequest, SourceRemoveRequest),
  483:     JingleEl = exml_query:subelement(SourceRemoveRequest, <<"jingle">>),
  484:     ?assertEqual(<<"source-remove">>,
  485:                  (exml_query:attr(JingleEl, <<"action">>))).
  486: 
  487: assert_source_add_action(SourceRemoveRequest, InviteRequest) ->
  488:     assert_same_sid(InviteRequest, SourceRemoveRequest),
  489:     JingleEl = exml_query:subelement(SourceRemoveRequest, <<"jingle">>),
  490:     ?assertEqual(<<"source-add">>,
  491:                  (exml_query:attr(JingleEl, <<"action">>))).
  492: 
  493: assert_source_update_action(SourceRemoveRequest, InviteRequest) ->
  494:     assert_same_sid(InviteRequest, SourceRemoveRequest),
  495:     JingleEl = exml_query:subelement(SourceRemoveRequest, <<"jingle">>),
  496:     ?assertEqual(<<"source-update">>,
  497:                  (exml_query:attr(JingleEl, <<"action">>))).
  498: 
  499: jingle_stanza_addressed_to_bare_jid_is_delivered(Config) ->
  500:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  501:         BobBareJID = escalus_client:short_jid(Bob),
  502:         Stanza = escalus_stanza:to(jingle_initiate(), BobBareJID),
  503:         escalus:send(Alice, Stanza),
  504:         R = escalus:wait_for_stanza(Bob),
  505:         escalus:assert(is_iq_set, R),
  506:         ?assertEqual(exml_query:attr(Stanza, <<"id">>), exml_query:attr(R, <<"id">>))
  507:     end).
  508: 
  509: jingle_stanza_addressed_to_own_bare_jid_is_rejected(Config) ->
  510:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  511:         AliceBareJID = escalus_client:short_jid(Alice),
  512:         Stanza = escalus_stanza:to(jingle_initiate(), AliceBareJID),
  513:         escalus:send(Alice, Stanza),
  514:         R = escalus:wait_for_stanza(Alice),
  515:         escalus:assert(is_error, [<<"modify">>, <<"bad-request">>], R)
  516:     end).
  517: 
  518: 
  519: other_iq_stanza_addressed_to_bare_jid_are_not_routed(Config) ->
  520:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  521:         BobBareJID = escalus_client:short_jid(Bob),
  522:         Stanza = escalus_stanza:to(escalus_stanza:iq_set(<<"urn:unknown:iq:ns">>, []), BobBareJID),
  523:         escalus:send(Alice, Stanza),
  524:         Reply = escalus:wait_for_stanza(Alice),
  525:         escalus:assert(is_error, [<<"cancel">>, <<"service-unavailable">>], Reply)
  526: 
  527:     end).
  528: 
  529: 
  530: jingle_initiate() ->
  531:     Audio = content(audio),
  532:     Video = content(video),
  533:     I = jingle_element(<<"session-initiate">>, [Audio, Video, content_group([Audio, Video])]),
  534:     iq_set(I).
  535: 
  536: iq_set(I) ->
  537:     Stanza = escalus_stanza:iq_set_nonquery(<<"jabber:client">>, [I]),
  538:     iq_with_id(Stanza).
  539: 
  540: iq_with_id(#xmlel{attrs = Attrs} = Stanza) ->
  541:     NewAttrs = lists:keystore(<<"id">>, 1, Attrs, {<<"id">>, uuid:uuid_to_string(uuid:get_v4(), binary_standard)}),
  542:     Stanza#xmlel{attrs = NewAttrs}.
  543: 
  544: iq_get(I) ->
  545:     Stanza = #xmlel{name = <<"iq">>,
  546:                     attrs = [{<<"xmlns">>, <<"jabber:client">>},
  547:                              {<<"type">>, <<"get">>}],
  548:                     children = [I]},
  549:     iq_with_id(Stanza).
  550: 
  551: jingle_element(Action, Children) ->
  552:     SID = uuid:uuid_to_string(uuid:get_v4(), binary_standard),
  553:     jingle_element(SID, Action, Children).
  554: 
  555: jingle_element(SID, Action, Children) ->
  556:     #xmlel{name = <<"jingle">>,
  557:            attrs = [{<<"action">>, Action},
  558:                     {<<"sid">>, SID},
  559:                     {<<"xmlns">>, <<"urn:xmpp:jingle:1">>}],
  560:            children = Children}.
  561: 
  562: jingle_accept(InviteRequest) ->
  563:     SID = exml_query:path(InviteRequest, path_to_jingle_sid()),
  564:     Audio = content(audio),
  565:     Video = content(video_disabled),
  566:     I = jingle_element(SID, <<"session-accept">>, [Audio, Video, content_group([Audio])]),
  567:     iq_set(I).
  568: 
  569: 
  570: jingle_source_remove(InviteRequest) ->
  571:     SID = exml_query:path(InviteRequest, path_to_jingle_sid()),
  572:     Audio = content(audio_source_remove),
  573:     Video = content(video_source_remove),
  574:     I = jingle_element(SID, <<"source-remove">>, [Audio, Video,
  575:                                                   content_group([Audio, Video])]),
  576:     iq_set(I).
  577: 
  578: jingle_source_add(InviteRequest) ->
  579:     SID = exml_query:path(InviteRequest, path_to_jingle_sid()),
  580:     Audio = content(audio_source_remove),
  581:     Video = content(video_source_remove),
  582:     I = jingle_element(SID, <<"source-add">>, [Audio, Video,
  583:                                                content_group([Audio, Video])]),
  584:     iq_set(I).
  585: 
  586: jingle_source_update(InviteRequest) ->
  587:     SID = exml_query:path(InviteRequest, path_to_jingle_sid()),
  588:     Audio = content(audio_source_remove),
  589:     Video = content(video_source_remove),
  590:     I = jingle_element(SID, <<"source-update">>, [Audio, Video,
  591:                                                   content_group([Audio, Video])]),
  592:     iq_set(I).
  593: 
  594: jingle_transport_info(InviteRequest, Creator, Media, TransportAttrs) ->
  595:     SID = exml_query:path(InviteRequest, path_to_jingle_sid()),
  596:     iq_set(jingle_element(SID, <<"transport-info">>, [trickle_ice_candidate(Creator, Media, TransportAttrs)])).
  597: 
  598: jingle_terminate(InviteRequest, Reason) ->
  599:     SID = exml_query:path(InviteRequest, path_to_jingle_sid()),
  600:     ReasonEl = #xmlel{name = <<"reason">>,
  601:                       children = [#xmlel{name = Reason}]},
  602:     iq_set(jingle_element(SID, <<"session-terminate">>, [ReasonEl])).
  603: 
  604: get_ice_candidates() ->
  605:     [
  606:      [{<<"foundation">>, <<"1293499931">>}, {<<"component">>, <<"1">>}, {<<"protocol">>, <<"udp">>}, {<<"priority">>, <<"2122260223">>}, {<<"ip">>, <<"172.86.160.16">>}, {<<"port">>, <<"46515">>}, {<<"type">>, <<"host">>}, {<<"generation">>, <<"0">>}, {<<"network">>, <<"1">>}, {<<"id">>, <<"1.1947885zlx">>}]
  607:     ].
  608: 
  609: trickle_ice_candidate(Creator, Content, TransportAttrs) ->
  610:     Candidate = #xmlel{name = <<"candidate">>,
  611:                        attrs = TransportAttrs},
  612:     Transport = #xmlel{name = <<"transport">>,
  613:                        attrs = [{<<"xmlns">>, <<"urn:xmpp:jingle:transports:ice-udp:1">>},
  614:                                 {<<"ufrag">>, <<"7Gpn">>},
  615:                                 {<<"pwd">>, <<"MUOzzatqL2qP7n1uRC7msD+c">>}],
  616:                        children = [Candidate]},
  617:     #xmlel{name = <<"content">>,
  618:            attrs = [{<<"name">>, Content},
  619:                     {<<"creator">>, Creator}],
  620:            children = [Transport]}.