1: -module(disco_and_caps_SUITE).
    2: -compile([export_all, nowarn_export_all]).
    3: 
    4: -include_lib("eunit/include/eunit.hrl").
    5: -include_lib("escalus/include/escalus_xmlns.hrl").
    6: 
    7: -import(domain_helper, [host_type/0, domain/0]).
    8: -import(config_parser_helper, [default_mod_config/1, mod_config/2, mod_config_with_auto_backend/1]).
    9: 
   10: all() ->
   11:     [{group, disco_with_caps},
   12:      {group, disco_with_caps_and_extra_features},
   13:      {group, disco_with_extra_features}].
   14: 
   15: groups() ->
   16:     G = [{disco_with_caps, [parallel], basic_test_cases() ++ caps_test_cases()},
   17:          {disco_with_caps_and_extra_features, [parallel],
   18:           basic_test_cases() ++ caps_test_cases() ++ extra_feature_test_cases()},
   19:          {disco_with_extra_features, [parallel], basic_test_cases() ++ extra_feature_test_cases()}],
   20:     ct_helper:repeat_all_until_all_ok(G).
   21: 
   22: basic_test_cases() ->
   23:     [user_cannot_query_stranger_resources,
   24:      user_cannot_query_stranger_features,
   25:      user_can_query_friend_resources,
   26:      user_can_query_friend_features,
   27:      user_cannot_query_own_resources_with_unknown_node,
   28:      user_cannot_query_friend_resources_with_unknown_node,
   29:      user_can_query_server_features].
   30: 
   31: caps_test_cases() ->
   32:     [caps_feature_is_advertised,
   33:      user_can_query_server_caps_via_disco].
   34: 
   35: extra_feature_test_cases() ->
   36:     [user_can_query_extra_domains,
   37:      user_can_query_server_info].
   38: 
   39: init_per_suite(C) ->
   40:     C.
   41: 
   42: end_per_suite(C) ->
   43:     escalus_fresh:clean(),
   44:     escalus:end_per_suite(C).
   45: 
   46: init_per_group(Name, C) ->
   47:     C2 = escalus:init_per_suite(dynamic_modules:save_modules(host_type(), C)),
   48:     dynamic_modules:ensure_modules(host_type(), required_modules(Name)),
   49:     C2.
   50: 
   51: end_per_group(_Name, C) ->
   52:     dynamic_modules:restore_modules(C).
   53: 
   54: init_per_testcase(Name, C) ->
   55:     escalus:init_per_testcase(Name, C).
   56: 
   57: end_per_testcase(Name, C) ->
   58:     escalus:end_per_testcase(Name, C).
   59: 
   60: caps_feature_is_advertised(Config) ->
   61:     Spec = escalus_users:get_userspec(Config, alice),
   62:     {ok, Connection, Features} = escalus_connection:start(Spec, [start_stream, stream_features]),
   63:     true = is_map(proplists:get_value(caps, Features)),
   64:     escalus_connection:stop(Connection).
   65: 
   66: user_can_query_server_caps_via_disco(Config) ->
   67:     NewConfig = escalus_fresh:create_users(Config, [{alice, 1}]),
   68:     Spec = escalus_users:get_userspec(NewConfig, alice),
   69:     {ok, Alice, Features} = escalus_connection:start(Spec),
   70:     #{<<"node">> := Node,
   71:       <<"ver">> := Ver} = proplists:get_value(caps, Features),
   72:     NodeVer = <<Node/binary, $#, Ver/binary>>,
   73:     Server = proplists:get_value(server, Spec),
   74:     Disco = escalus_stanza:disco_info(Server, NodeVer),
   75:     escalus:send(Alice, Disco),
   76:     DiscoResp = escalus:wait_for_stanza(Alice),
   77:     escalus:assert(is_iq_result, [Disco], DiscoResp),
   78:     Identity  = exml_query:path(DiscoResp, [{element, <<"query">>},
   79:                                             {element, <<"identity">>},
   80:                                             {attr, <<"name">>}]),
   81:     <<"MongooseIM">> = Identity,
   82:     escalus_connection:stop(Alice).
   83: 
   84: user_cannot_query_stranger_resources(Config) ->
   85:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
   86:         BobJid = escalus_client:short_jid(Bob),
   87:         Request = escalus_stanza:disco_items(BobJid),
   88:         escalus:send(Alice, Request),
   89:         Stanza = escalus:wait_for_stanza(Alice),
   90:         escalus:assert(is_iq_error, [Request], Stanza),
   91:         escalus:assert(is_error, [<<"cancel">>, <<"service-unavailable">>], Stanza),
   92:         escalus:assert(is_stanza_from, [BobJid], Stanza)
   93:     end).
   94: 
   95: user_cannot_query_stranger_features(Config) ->
   96:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
   97:         BobJid = escalus_client:short_jid(Bob),
   98:         Request = escalus_stanza:disco_info(BobJid),
   99:         escalus:send(Alice, Request),
  100:         Stanza = escalus:wait_for_stanza(Alice),
  101:         escalus:assert(is_iq_error, [Request], Stanza),
  102:         escalus:assert(is_error, [<<"cancel">>, <<"service-unavailable">>], Stanza),
  103:         escalus:assert(is_stanza_from, [BobJid], Stanza)
  104:     end).
  105: 
  106: user_can_query_friend_resources(Config) ->
  107:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  108:         escalus_story:make_all_clients_friends([Alice, Bob]),
  109:         BobJid = escalus_client:short_jid(Bob),
  110:         escalus:send(Alice, escalus_stanza:disco_items(BobJid)),
  111:         Stanza = escalus:wait_for_stanza(Alice),
  112:         Query = exml_query:subelement(Stanza, <<"query">>),
  113:         BobFullJid = escalus_client:full_jid(Bob),
  114:         BobName = escalus_client:username(Bob),
  115:         Item = exml_query:subelement_with_attr(Query, <<"jid">>, BobFullJid),
  116:         ?assertEqual(BobName, exml_query:attr(Item, <<"name">>)),
  117:         escalus:assert(is_stanza_from, [BobJid], Stanza)
  118:     end).
  119: 
  120: user_can_query_friend_features(Config) ->
  121:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  122:         escalus_story:make_all_clients_friends([Alice, Bob]),
  123:         BobJid = escalus_client:short_jid(Bob),
  124:         escalus:send(Alice, escalus_stanza:disco_info(BobJid)),
  125:         Stanza = escalus:wait_for_stanza(Alice),
  126:         escalus:assert(has_identity, [<<"account">>, <<"registered">>], Stanza),
  127:         escalus:assert(is_stanza_from, [BobJid], Stanza)
  128:     end).
  129: 
  130: user_cannot_query_own_resources_with_unknown_node(Config) ->
  131:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  132:         AliceJid = escalus_client:short_jid(Alice),
  133:         Request = escalus_stanza:disco_items(AliceJid, <<"unknown-node">>),
  134:         escalus:send(Alice, Request),
  135:         Stanza = escalus:wait_for_stanza(Alice),
  136:         escalus:assert(is_iq_error, [Request], Stanza),
  137:         escalus:assert(is_error, [<<"cancel">>, <<"item-not-found">>], Stanza),
  138:         escalus:assert(is_stanza_from, [AliceJid], Stanza)
  139:     end).
  140: 
  141: user_cannot_query_friend_resources_with_unknown_node(Config) ->
  142:     escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) ->
  143:         escalus_story:make_all_clients_friends([Alice, Bob]),
  144:         BobJid = escalus_client:short_jid(Bob),
  145:         Request = escalus_stanza:disco_items(BobJid, <<"unknown-node">>),
  146:         escalus:send(Alice, Request),
  147:         Stanza = escalus:wait_for_stanza(Alice),
  148:         escalus:assert(is_iq_error, [Request], Stanza),
  149:         escalus:assert(is_error, [<<"cancel">>, <<"not-allowed">>], Stanza),
  150:         escalus:assert(is_stanza_from, [BobJid], Stanza)
  151:     end).
  152: 
  153: user_can_query_extra_domains(Config) ->
  154:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  155:         Server = escalus_client:server(Alice),
  156:         escalus:send(Alice, escalus_stanza:service_discovery(Server)),
  157:         Stanza = escalus:wait_for_stanza(Alice),
  158:         escalus:assert(has_service, [extra_domain()], Stanza),
  159:         escalus:assert(is_stanza_from, [domain()], Stanza)
  160:     end).
  161: 
  162: user_can_query_server_features(Config) ->
  163:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  164:         Server = escalus_client:server(Alice),
  165:         escalus:send(Alice, escalus_stanza:disco_info(Server)),
  166:         Stanza = escalus:wait_for_stanza(Alice),
  167:         escalus:assert(has_identity, [<<"server">>, <<"im">>], Stanza),
  168:         escalus:assert(has_feature, [<<"iq">>], Stanza),
  169:         escalus:assert(has_feature, [<<"presence">>], Stanza),
  170:         escalus:assert(is_stanza_from, [domain()], Stanza)
  171:     end).
  172: 
  173: %% XEP-0157: Contact Addresses for XMPP Services
  174: user_can_query_server_info(Config) ->
  175:     escalus:fresh_story(Config, [{alice, 1}], fun(Alice) ->
  176:         Server = escalus_client:server(Alice),
  177:         escalus:send(Alice, escalus_stanza:disco_info(Server)),
  178:         Stanza = escalus:wait_for_stanza(Alice),
  179:         escalus:assert(is_stanza_from, [domain()], Stanza),
  180: 
  181:         %% 'sales' is hidden for mod_disco, so only 'abuse' and 'admin' are expected
  182:         [HiddenField, AbuseField, AdminField] = get_form_fields(Stanza),
  183:         ?assertEqual(<<"FORM_TYPE">>, exml_query:attr(HiddenField, <<"var">>)),
  184:         ?assertEqual(<<"hidden">>, exml_query:attr(HiddenField, <<"type">>)),
  185:         ?assertEqual([?NS_SERVERINFO], exml_query:paths(HiddenField, [{element, <<"value">>},
  186:                                                                       cdata])),
  187:         ?assertEqual(name(abuse), exml_query:attr(AbuseField, <<"var">>)),
  188:         ?assertEqual(urls(abuse), exml_query:paths(AbuseField, [{element, <<"value">>},
  189:                                                                               cdata])),
  190:         ?assertEqual(name(admin), exml_query:attr(AdminField, <<"var">>)),
  191:         ?assertEqual(urls(admin), exml_query:paths(AdminField, [{element, <<"value">>},
  192:                                                                 cdata]))
  193:     end).
  194: 
  195: %% Helpers
  196: 
  197: required_modules(disco_with_caps) ->
  198:     [{mod_caps, mod_config_with_auto_backend(mod_caps)},
  199:      {mod_disco, default_mod_config(mod_disco)}];
  200: required_modules(disco_with_caps_and_extra_features) ->
  201:     [{mod_caps, mod_config_with_auto_backend(mod_caps)},
  202:      {mod_disco, mod_config(mod_disco, extra_disco_opts())}];
  203: required_modules(disco_with_extra_features) ->
  204:     [{mod_disco, mod_config(mod_disco, extra_disco_opts())}].
  205: 
  206: extra_disco_opts() ->
  207:     #{extra_domains => [extra_domain()],
  208:       server_info => [server_info(abuse, #{}),
  209:                       server_info(admin, #{modules => [mod_disco]}),
  210:                       server_info(sales, #{modules => [mod_pubsub]})]}.
  211: 
  212: get_form_fields(Stanza) ->
  213:      exml_query:paths(Stanza, [{element_with_ns, <<"query">>, ?NS_DISCO_INFO},
  214:                                {element_with_ns, <<"x">>, ?NS_DATA_FORMS},
  215:                                {element, <<"field">>}]).
  216: 
  217: extra_domain() ->
  218:     <<"eXtra.example.com">>.
  219: 
  220: server_info(Type, Extra) ->
  221:     maps:merge(#{name => name(Type), urls => urls(Type)}, Extra).
  222: 
  223: name(abuse) -> <<"abuse-addresses">>;
  224: name(admin) -> <<"admin-addresses">>;
  225: name(sales) -> <<"sales-addresses">>.
  226: 
  227: urls(abuse) -> [<<"abuse@example.com">>];
  228: urls(admin) -> [<<"admin@example.com">>, <<"operations@example.com">>];
  229: urls(sales) -> [<<"sales@example.com">>].