1: %%==============================================================================
    2: %% Copyright 2020 Erlang Solutions Ltd.
    3: %%
    4: %% Licensed under the Apache License, Version 2.0 (the "License");
    5: %% you may not use this file except in compliance with the License.
    6: %% You may obtain a copy of the License at
    7: %%
    8: %% http://www.apache.org/licenses/LICENSE-2.0
    9: %%
   10: %% Unless required by applicable law or agreed to in writing, software
   11: %% distributed under the License is distributed on an "AS IS" BASIS,
   12: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   13: %% See the License for the specific language governing permissions and
   14: %% limitations under the License.
   15: %%==============================================================================
   16: -module(extdisco_SUITE).
   17: 
   18: -include_lib("common_test/include/ct.hrl").
   19: -include_lib("exml/include/exml.hrl").
   20: -include_lib("eunit/include/eunit.hrl").
   21: 
   22: -import(distributed_helper, [mim/0,
   23:                              rpc/4]).
   24: 
   25: -import(domain_helper, [domain/0]).
   26: 
   27: -define(NS_EXTDISCO, <<"urn:xmpp:extdisco:2">>).
   28: 
   29: -compile([export_all, nowarn_export_all]).
   30: 
   31: all() ->
   32:     [{group, extdisco_not_configured},
   33:      {group, extdisco_configured},
   34:      {group, multiple_extdisco_configured},
   35:      {group, extdisco_required_elements_configured}].
   36: 
   37: groups() ->
   38:     G = [{extdisco_not_configured, [sequence], extdisco_not_configured_tests()},
   39:          {extdisco_configured, [sequence], extdisco_configured_tests()},
   40:          {multiple_extdisco_configured, [sequence], multiple_extdisco_configured_tests()},
   41:          {extdisco_required_elements_configured, [sequence], extdisco_required_elements_configured_tests()}],
   42:     ct_helper:repeat_all_until_all_ok(G).
   43: 
   44: extdisco_not_configured_tests() ->
   45:     [external_services_discovery_not_supported,
   46:      no_external_services_configured_no_services_returned].
   47: 
   48: extdisco_configured_tests() ->
   49:     tests().
   50: 
   51: multiple_extdisco_configured_tests() ->
   52:     tests().
   53: 
   54: tests() ->
   55:     [external_services_discovery_supported,
   56:      external_services_configured_all_returned,
   57:      external_services_configured_only_matching_by_type_returned,
   58:      external_services_configured_no_matching_services_no_returned,
   59:      external_services_configured_credentials_returned,
   60:      external_services_configured_no_matching_credentials_no_returned,
   61:      external_services_configured_no_matching_credentials_type_no_returned,
   62:      external_services_configured_incorrect_request_no_returned].
   63: 
   64: extdisco_required_elements_configured_tests() ->
   65:     [external_service_required_elements_configured].
   66: 
   67: init_per_suite(Config) ->
   68:     NewConfig = dynamic_modules:save_modules(domain(), Config),
   69:     escalus:init_per_suite(NewConfig).
   70: 
   71: init_per_group(extdisco_configured, Config) ->
   72:     ExternalServices = [stun_service()],
   73:     set_external_services(ExternalServices, Config);
   74: init_per_group(multiple_extdisco_configured, Config) ->
   75:     ExternalServices = [stun_service(), stun_service(), turn_service()],
   76:     set_external_services(ExternalServices, Config);
   77: init_per_group(extdisco_required_elements_configured, Config) ->
   78:     ExternalServices = [[{type, ftp},{host, "3.3.3.3"}]],
   79:     set_external_services(ExternalServices, Config);
   80: init_per_group(_GroupName, Config) ->
   81:    Config.
   82: 
   83: init_per_testcase(external_services_discovery_not_supported = Name, Config) ->
   84:     NewConfig = remove_external_services(Config),
   85:     escalus:init_per_testcase(Name, NewConfig);
   86: init_per_testcase(no_external_services_configured_no_services_returned = Name, Config) ->
   87:     ExternalServices = [],
   88:     NewConfig = set_external_services(ExternalServices, Config),
   89:     escalus:init_per_testcase(Name, NewConfig);
   90: init_per_testcase(Name, Config) ->
   91:     escalus:init_per_testcase(Name, Config).
   92: 
   93: end_per_testcase(Name, Config) when
   94:     Name == external_services_discovery_not_supported;
   95:     Name == no_external_services_configured_no_services_returned ->
   96:     dynamic_modules:restore_modules(Config),
   97:     escalus:end_per_testcase(Name, Config);
   98: end_per_testcase(Name, Config) ->
   99:     escalus:end_per_testcase(Name, Config).
  100: 
  101: end_per_group(_GroupName, Config) ->
  102:     dynamic_modules:restore_modules(Config),
  103:     Config.
  104: 
  105: end_per_suite(Config) ->
  106:     escalus_fresh:clean(),
  107:     escalus:end_per_suite(Config).
  108: 
  109: %%--------------------------------------------------------------------
  110: %% TEST CASES
  111: %%--------------------------------------------------------------------
  112: 
  113: external_services_discovery_not_supported(Config) ->
  114:     % Given external service discovery is not configured
  115:     Test = fun(Alice) ->
  116: 
  117:        % When requesting for disco_info
  118:        IqGet = escalus_stanza:disco_info(domain()),
  119:        escalus_client:send(Alice, IqGet),
  120: 
  121:        % Then extdisco feature is not listed as supported feature
  122:        Result = escalus_client:wait_for_stanza(Alice),
  123:        escalus:assert(is_iq_result, [IqGet], Result),
  124:        escalus:assert(fun(Stanza) ->
  125:            not escalus_pred:has_feature(?NS_EXTDISCO, Stanza)
  126:            end, Result)
  127:     end,
  128:     escalus:fresh_story(Config, [{alice, 1}], Test).
  129: 
  130: no_external_services_configured_no_services_returned(Config) ->
  131:     % Given external service discovery is configured with empty list
  132:     Test = fun(Alice) ->
  133: 
  134:         % When requesting for external services
  135:         Iq = request_external_services(domain()),
  136:         escalus_client:send(Alice, Iq),
  137: 
  138:         % Then services but no service element is in the iq result,
  139:         % which means that empty services element got returned
  140:         Result = escalus_client:wait_for_stanza(Alice),
  141:         escalus:assert(is_iq_result, [Iq], Result),
  142:         ?assertEqual(true, has_subelement_with_ns(Result, <<"services">>, ?NS_EXTDISCO)),
  143:         ?assertEqual([], get_service_element(Result))
  144:     end,
  145:     escalus:fresh_story(Config, [{alice, 1}], Test).
  146: 
  147: external_services_discovery_supported(Config) ->
  148:     % Given external service discovery is configured
  149:     Test = fun(Alice) ->
  150: 
  151:         % When requesting for disco_info
  152:         IqGet = escalus_stanza:disco_info(domain()),
  153:         escalus_client:send(Alice, IqGet),
  154: 
  155:         % Then extdisco feature is listed as supported feature
  156:         Result = escalus_client:wait_for_stanza(Alice),
  157:         escalus:assert(is_iq_result, [IqGet], Result),
  158:         escalus:assert(has_feature, [?NS_EXTDISCO], Result)
  159:     end,
  160:     escalus:fresh_story(Config, [{alice, 1}], Test).
  161: 
  162: external_services_configured_all_returned(Config) ->
  163:     % Given external service discovery is configured
  164:     Test = fun(Alice) ->
  165: 
  166:         % When requesting for external services
  167:         Iq = request_external_services(domain()),
  168:         escalus_client:send(Alice, Iq),
  169: 
  170:         % Then list of external services with containing all
  171:         % supported_elements() is returned
  172:         Result = escalus_client:wait_for_stanza(Alice),
  173:         escalus:assert(is_iq_result, [Iq], Result),
  174:         ?assertEqual(true, has_subelement_with_ns(Result, <<"services">>, ?NS_EXTDISCO)),
  175:         [all_services_are_returned(Service) || Service <- get_service_element(Result)]
  176:     end,
  177:     escalus:fresh_story(Config, [{alice, 1}], Test).
  178: 
  179: external_services_configured_only_matching_by_type_returned(Config) ->
  180:     % Given external service discovery is configured
  181:     Test = fun(Alice) ->
  182: 
  183:         % When requesting for external service of specified type
  184:         Type = <<"stun">>,
  185:         Iq = request_external_services_with_type(domain(), Type),
  186:         escalus_client:send(Alice, Iq),
  187: 
  188:         % Then the list of external services of the specified type is returned
  189:         Result = escalus_client:wait_for_stanza(Alice),
  190:         escalus:assert(is_iq_result, [Iq], Result),
  191:         ?assertEqual(true, has_subelement_with_ns(Result, <<"services">>, ?NS_EXTDISCO)),
  192:         [all_services_are_returned(Service, Type) || Service <- get_service_element(Result)]
  193:     end,
  194:     escalus:fresh_story(Config, [{alice, 1}], Test).
  195: 
  196: external_services_configured_no_matching_services_no_returned(Config) ->
  197:     % Given external service discovery is configured
  198:     Test = fun(Alice) ->
  199: 
  200:         % When requesting for external service of unknown or unconfigured type
  201:         Type = <<"unknown_service">>,
  202:         Iq = request_external_services_with_type(domain(), Type),
  203:         escalus_client:send(Alice, Iq),
  204: 
  205:         % Then the iq_errror is returned
  206:         Result = escalus_client:wait_for_stanza(Alice),
  207:         escalus:assert(is_iq_error, [Iq], Result)
  208:     end,
  209: escalus:fresh_story(Config, [{alice, 1}], Test).
  210: 
  211: external_services_configured_credentials_returned(Config) ->
  212:     % Given external service discovery is configured with credentials
  213:     Test = fun(Alice) ->
  214: 
  215:         % When requesting for credentials of external service of given type
  216:         % and specified host
  217:         Type = <<"stun">>,
  218:         Host = <<"1.1.1.1">>,
  219:         Iq = request_external_services_credentials(domain(), Type, Host),
  220:         escalus_client:send(Alice, Iq),
  221: 
  222:         % Then the list of external services of the specified type and host
  223:         % is returned together with STUN/TURN login credentials
  224:         Result = escalus_client:wait_for_stanza(Alice),
  225:         escalus:assert(is_iq_result, [Iq], Result),
  226:         ?assertEqual(true, has_subelement_with_ns(Result, <<"credentials">>, ?NS_EXTDISCO)),
  227:         Services = get_service_element(Result),
  228:         ?assertNotEqual([], Services),
  229:         [all_services_are_returned(Service, Type) || Service <- Services]
  230:     end,
  231:     escalus:fresh_story(Config, [{alice, 1}], Test).
  232: 
  233: external_services_configured_no_matching_credentials_no_returned(Config) ->
  234:     % Given external service discovery is configured with credentials
  235:     Test = fun(Alice) ->
  236: 
  237:         % When requesting for credentials of external service of unknown type
  238:         % and unknown host
  239:         Type = <<"unknown_service">>,
  240:         Host = <<"unknown_host">>,
  241:         Iq = request_external_services_credentials(domain(), Type, Host),
  242:         escalus_client:send(Alice, Iq),
  243: 
  244:         % Then iq_error is retured
  245:         Result = escalus_client:wait_for_stanza(Alice),
  246:         escalus:assert(is_iq_error, [Iq], Result)
  247:     end,
  248:     escalus:fresh_story(Config, [{alice, 1}], Test).
  249: 
  250: external_services_configured_no_matching_credentials_type_no_returned(Config) ->
  251:     % Given external service discovery is configured with credentials
  252:     Test = fun(Alice) ->
  253: 
  254:         % When requesting for credentials of external service without defining
  255:         % the service type
  256:         Host = <<"stun1">>,
  257:         Iq = request_external_services_credentials_host_only(domain(), Host),
  258:         escalus_client:send(Alice, Iq),
  259: 
  260:         % Then iq_error is retured
  261:         Result = escalus_client:wait_for_stanza(Alice),
  262:         escalus:assert(is_iq_error, [Iq], Result)
  263:     end,
  264:     escalus:fresh_story(Config, [{alice, 1}], Test).
  265: 
  266: external_services_configured_incorrect_request_no_returned(Config) ->
  267:     % Given external service discovery is configured
  268:     Test = fun(Alice) ->
  269: 
  270:         % When sending request with incorrect elements
  271:         Iq = request_external_services_incorrect(domain()),
  272:         escalus_client:send(Alice, Iq),
  273: 
  274:         % Then iq_error is returned
  275:         Result = escalus_client:wait_for_stanza(Alice),
  276:         escalus:assert(is_iq_error, [Iq], Result)
  277:     end,
  278:     escalus:fresh_story(Config, [{alice, 1}], Test).
  279: 
  280: external_service_required_elements_configured(Config) ->
  281:     % Given external service discovery is configured only with required elements
  282:     Test = fun(Alice) ->
  283: 
  284:         % When requesting for external services
  285:         Iq = request_external_services(domain()),
  286:         escalus_client:send(Alice, Iq),
  287: 
  288:         % Then list of external services with containing all
  289:         % required_elements() is returned
  290:         Result = escalus_client:wait_for_stanza(Alice),
  291:         escalus:assert(is_iq_result, [Iq], Result),
  292:         ?assertEqual(true, has_subelement_with_ns(Result, <<"services">>, ?NS_EXTDISCO)),
  293:         [required_services_are_returned(Service) || Service <- get_service_element(Result)]
  294:     end,
  295:     escalus:fresh_story(Config, [{alice, 1}], Test).
  296: 
  297: %%-----------------------------------------------------------------
  298: %% Helpers
  299: %%-----------------------------------------------------------------
  300: 
  301: stun_service() ->
  302:       [{type, stun},
  303:        {host, "1.1.1.1"},
  304:        {port, 3478},
  305:        {transport, "udp"},
  306:        {username, "username"},
  307:        {password, "secret"}].
  308: 
  309: turn_service() ->
  310:        [{type, turn},
  311:         {host, "2.2.2.2"},
  312:         {port, 3478},
  313:         {transport, "tcp"},
  314:         {username, "username"},
  315:         {password, "secret"}].
  316: 
  317: set_external_services(Opts, Config) ->
  318:     Module = [{mod_extdisco, Opts}],
  319:     ok = dynamic_modules:ensure_modules(domain(), Module),
  320:     Config.
  321: 
  322: remove_external_services(Config) ->
  323:     dynamic_modules:ensure_stopped(domain(), [mod_extdisco]),
  324:     Config.
  325: 
  326: request_external_services(To) ->
  327:     escalus_stanza:iq(To, <<"get">>,
  328:         [#xmlel{name = <<"services">>,
  329:                 attrs = [{<<"xmlns">>, <<"urn:xmpp:extdisco:2">>}]}]).
  330: 
  331: request_external_services_with_type(To, Type) ->
  332:     escalus_stanza:iq(To, <<"get">>,
  333:         [#xmlel{name = <<"services">>,
  334:                 attrs = [{<<"xmlns">>, <<"urn:xmpp:extdisco:2">>},
  335:                          {<<"type">>, Type}]}]).
  336: 
  337: request_external_services_credentials(To, Type, Host) ->
  338:     escalus_stanza:iq(To, <<"get">>,
  339:         [#xmlel{name = <<"credentials">>,
  340:                 attrs = [{<<"xmlns">>, <<"urn:xmpp:extdisco:2">>}],
  341:                 children = [#xmlel{name = <<"service">>,
  342:                                    attrs = [{<<"host">>, Host},
  343:                                             {<<"type">>, Type}]}]}]).
  344: 
  345: request_external_services_credentials_host_only(To, Host) ->
  346:     escalus_stanza:iq(To, <<"get">>,
  347:         [#xmlel{name = <<"credentials">>,
  348:                 attrs = [{<<"xmlns">>, <<"urn:xmpp:extdisco:2">>}],
  349:                 children = [#xmlel{name = <<"service">>,
  350:                                    attrs = [{<<"host">>, Host}]}]}]).
  351: 
  352: request_external_services_incorrect(To) ->
  353:     escalus_stanza:iq(To, <<"get">>,
  354:         [#xmlel{name = <<"incorrect">>,
  355:                 attrs = [{<<"xmlns">>, <<"urn:xmpp:extdisco:2">>}]}]).
  356: 
  357: get_service_element(Result) ->
  358:     Services = exml_query:subelement_with_ns(Result, ?NS_EXTDISCO),
  359:     exml_query:subelements(Services, <<"service">>).
  360: 
  361: required_elements() ->
  362:     [<<"host">>, <<"type">>].
  363: 
  364: supported_elements() ->
  365:     required_elements() ++ [<<"port">>, <<"username">>, <<"password">>].
  366: 
  367: all_services_are_returned(Service) ->
  368:     [?assertEqual(true, has_subelement(Service, E)) || E <- supported_elements()].
  369: 
  370: required_services_are_returned(Service) ->
  371:     [?assertEqual(true, has_subelement(Service, E)) || E <- required_elements()].
  372: 
  373: all_services_are_returned(Service, Type) ->
  374:     ?assertEqual(true, has_attr_with_value(Service, <<"type">>, Type)),
  375:     all_services_are_returned(Service).
  376: 
  377: no_services_are_returned(Service) ->
  378:     [?assertEqual(false, has_subelement(Service, E)) || E <- supported_elements()].
  379: 
  380: has_subelement(Stanza, Element) ->
  381:     undefined =/= exml_query:attr(Stanza, Element).
  382: 
  383: has_attr_with_value(Stanza, Element, Value) ->
  384:     Value == exml_query:attr(Stanza, Element).
  385: 
  386: has_subelement_with_ns(Stanza, Element, NS) ->
  387:     [] =/= exml_query:subelements_with_name_and_ns(Stanza, Element, NS).