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