1: %%==============================================================================
    2: %% Copyright 2013 Erlang Solutions Ltd.
    3: %%
    4: %% Test the mod_vcard running on the server.
    5: %%
    6: %%
    7: %% Licensed under the Apache License, Version 2.0 (the "License");
    8: %% you may not use this file except in compliance with the License.
    9: %% You may obtain a copy of the License at
   10: %%
   11: %% http://www.apache.org/licenses/LICENSE-2.0
   12: %%
   13: %% Unless required by applicable law or agreed to in writing, software
   14: %% distributed under the License is distributed on an "AS IS" BASIS,
   15: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   16: %% See the License for the specific language governing permissions and
   17: %% limitations under the License.
   18: %%==============================================================================
   19: 
   20: -module(vcard_simple_SUITE).
   21: -compile([export_all, nowarn_export_all]).
   22: 
   23: -include_lib("escalus/include/escalus_xmlns.hrl").
   24: -include_lib("escalus/include/escalus.hrl").
   25: -include_lib("common_test/include/ct.hrl").
   26: -include_lib("exml/include/exml.hrl").
   27: 
   28: %% Element CData
   29: -define(EL(Element, Name), exml_query:path(Element, [{element, Name}])).
   30: -define(EL_CD(Element, Name), exml_query:path(Element, [{element, Name}, cdata])).
   31: 
   32: -import(vcard_helper, [is_vcard_ldap/0]).
   33: 
   34: -import(distributed_helper, [mim/0,
   35:                              require_rpc_nodes/1,
   36:                              subhost_pattern/1,
   37:                              rpc/4]).
   38: -import(domain_helper, [host_type/0,
   39:                         host_types/0,
   40:                         domain/0]).
   41: 
   42: %%--------------------------------------------------------------------
   43: %% Suite configuration
   44: %%--------------------------------------------------------------------
   45: 
   46: all() ->
   47:     [{group, all}
   48:     ].
   49: 
   50: groups() ->
   51:     %% setting test data before tests is proving awkward so might as well use the
   52:     %% data set in the update tests to test the rest.
   53:     G = [{all, [sequence], all_tests()}
   54:         ],
   55:     ct_helper:repeat_all_until_all_ok(G).
   56: 
   57: all_tests() ->
   58:     [update_own_card,
   59:      retrieve_own_card,
   60:      user_doesnt_exist,
   61:      update_other_card,
   62:      retrieve_others_card,
   63:      request_search_fields,
   64:      search_empty,
   65:      search_some,
   66:      search_wildcard].
   67: 
   68: suite() ->
   69:     require_rpc_nodes([mim]) ++ escalus:suite().
   70: 
   71: %%--------------------------------------------------------------------
   72: %% Init & teardown
   73: %%--------------------------------------------------------------------
   74: 
   75: init_per_suite(Config) ->
   76:     Config1 = prepare_vcard_module(escalus:init_per_suite(Config)),
   77:     configure_mod_vcard(Config1),
   78:     escalus:create_users(Config1, escalus:get_users([alice, bob])).
   79: 
   80: end_per_suite(Config) ->
   81:     NewConfig = escalus:delete_users(Config, escalus:get_users([alice, bob])),
   82:     restore_vcard_module(NewConfig),
   83:     escalus:end_per_suite(NewConfig).
   84: 
   85: init_per_group(_GN, Config) ->
   86:     Config.
   87: 
   88: end_per_group(_, Config) ->
   89:     Config.
   90: 
   91: init_per_testcase(CaseName, Config) ->
   92:     escalus:init_per_testcase(CaseName, Config).
   93: 
   94: end_per_testcase(CaseName, Config) ->
   95:     escalus:end_per_testcase(CaseName, Config).
   96: 
   97: 
   98: %%--------------------------------------------------------------------
   99: %% XEP-0054: vcard-temp Test cases
  100: %%--------------------------------------------------------------------
  101: 
  102: update_own_card(Config) ->
  103:     case is_vcard_ldap() of
  104:         true ->
  105:             {skip,ldap_vcard_is_readonly};
  106:         _ ->
  107:             escalus:story(
  108:               Config, [{alice, 1}],
  109:               fun(Client1) ->
  110:                       %% set some initial value different from the actual test data
  111:                       %% so we know it really got updated and wasn't just old data
  112:                       FN = get_FN(Config),
  113:                       Client1Fields = [{<<"FN">>, FN}],
  114:                       Client1SetResultStanza
  115:                       = escalus:send_and_wait(Client1,
  116:                                               escalus_stanza:vcard_update(Client1Fields)),
  117:                       escalus:assert(is_iq_result, Client1SetResultStanza),
  118:                       escalus_stanza:vcard_request(),
  119:                       Client1GetResultStanza
  120:                       = escalus:send_and_wait(Client1, escalus_stanza:vcard_request()),
  121:                       FN
  122:                       = stanza_get_vcard_field_cdata(Client1GetResultStanza, <<"FN">>)
  123:               end)
  124:     end.
  125: 
  126: retrieve_own_card(Config) ->
  127:     escalus:story(
  128:       Config, [{alice, 1}],
  129:       fun(Client) ->
  130:               Res = escalus:send_and_wait(Client,
  131:                         escalus_stanza:vcard_request()),
  132:               ClientVCardTups = [{<<"FN">>, get_FN(Config)}],
  133:               check_vcard(ClientVCardTups, Res)
  134:       end).
  135: 
  136: 
  137: 
  138: %% If no vCard exists, the server MUST return a stanza error
  139: %% (which SHOULD be <item-not-found/>) or an IQ-result
  140: %% containing an empty <vCard/> element.
  141: %% We return <item-not-found/>
  142: user_doesnt_exist(Config) ->
  143:     escalus:story(
  144:       Config, [{alice, 1}],
  145:       fun(Client) ->
  146:               Domain = domain(),
  147:               BadJID = <<"nonexistent@", Domain/binary>>,
  148:               Res = escalus:send_and_wait(Client,
  149:                         escalus_stanza:vcard_request(BadJID)),
  150:                 case
  151:                   escalus_pred:is_error(<<"cancel">>,
  152:                                         <<"item-not-found">>,
  153:                                         Res) of
  154:                   true ->
  155:                       ok;
  156:                   _ ->
  157:                       [] = Res#xmlel.children,
  158:                       ct:comment("empty result instead of error")
  159:               end
  160:       end).
  161: 
  162: update_other_card(Config) ->
  163:     escalus:story(
  164:       Config, [{alice, 1}, {bob, 1}],
  165:       fun(Client, OtherClient) ->
  166:               JID = escalus_client:short_jid(Client),
  167:               Fields = [{<<"FN">>, <<"New name">>}],
  168:               Res = escalus:send_and_wait(OtherClient,
  169:                         escalus_stanza:vcard_update(JID, Fields)),
  170: 
  171:               %% check that nothing was changed
  172:               Res2 = escalus:send_and_wait(Client,
  173:                         escalus_stanza:vcard_request()),
  174:               ClientVCardTups = [{<<"FN">>, get_FN(Config)}],
  175:               check_vcard(ClientVCardTups, Res2),
  176: 
  177:               case escalus_pred:is_error(<<"cancel">>,
  178:                                         <<"not-allowed">>, Res) of
  179:                   true ->
  180:                       ok;
  181:                   _ ->
  182:                         ct:comment("no error returned")
  183:               end
  184:       end).
  185: 
  186: retrieve_others_card(Config) ->
  187:     escalus:story(
  188:       Config, [{alice, 1}, {bob, 1}],
  189:       fun(Client, OtherClient) ->
  190:               JID = escalus_client:short_jid(Client),
  191:               Res = escalus:send_and_wait(OtherClient,
  192:                         escalus_stanza:vcard_request(JID)),
  193:               OtherClientVCardTups = [{<<"FN">>, get_FN(Config)}],
  194:               check_vcard(OtherClientVCardTups, Res),
  195: 
  196:               %% In accordance with XMPP Core [5], a compliant server MUST
  197:               %% respond on behalf of the requestor and not forward the IQ to
  198:               %% the requestee's connected resource.
  199: 
  200:               Res2 = (catch escalus:wait_for_stanza(Client)),
  201:               escalus:assert(stanza_timeout, Res2)
  202:       end).
  203: 
  204: %%--------------------------------------------------------------------
  205: %% XEP-0055 jabber:iq:search User Directory service Test cases
  206: %%
  207: %%--------------------------------------------------------------------
  208: 
  209: %% all.search.domain
  210: 
  211: request_search_fields(Config) ->
  212:     escalus:story(
  213:       Config, [{alice, 1}],
  214:       fun(Client) ->
  215:               Domain = domain(),
  216:               DirJID = <<"vjud.", Domain/binary>>,
  217:               Res = escalus:send_and_wait(Client,
  218:                                       escalus_stanza:search_fields_iq(DirJID)),
  219:               escalus:assert(is_iq_result, Res),
  220:               Result = ?EL(Res, <<"query">>),
  221:               XData = ?EL(Result, <<"x">>),
  222:               #xmlel{ children = XChildren } = XData,
  223:               FieldTups = field_tuples(XChildren),
  224:               true = lists:member({<<"text-single">>,
  225:                                    get_field_name(user), <<"User">>},
  226:                                   FieldTups),
  227:               true = lists:member({<<"text-single">>,
  228:                                    get_field_name(fn),
  229:                                    <<"Full Name">>},
  230:                                   FieldTups)
  231: 
  232:       end).
  233: 
  234: search_empty(Config) ->
  235:     escalus:story(
  236:       Config, [{alice, 1}],
  237:       fun(Client) ->
  238:               Domain = domain(),
  239:               DirJID = <<"vjud.", Domain/binary>>,
  240:               Fields = [{get_field_name(fn), <<"nobody">>}],
  241:               Res = escalus:send_and_wait(Client,
  242:                                 escalus_stanza:search_iq(DirJID,
  243:                                     escalus_stanza:search_fields(Fields))),
  244:               escalus:assert(is_iq_result, Res),
  245:               [] = search_result_item_tuples(Res)
  246:       end).
  247: 
  248: search_some(Config) ->
  249:     escalus:story(
  250:       Config, [{bob, 1}],
  251:       fun(Client) ->
  252:               Domain = domain(),
  253:               DirJID = <<"vjud.", Domain/binary>>,
  254:               Fields = [{get_field_name(fn), get_FN(Config)}],
  255:               Res = escalus:send_and_wait(Client,
  256:                                 escalus_stanza:search_iq(DirJID,
  257:                                     escalus_stanza:search_fields(Fields))),
  258:               escalus:assert(is_iq_result, Res),
  259: 
  260:               %% Basically test that the right values exist
  261:               %% and map to the right column headings
  262:               ItemTups = search_result_item_tuples(Res),
  263:               1 = length(ItemTups)
  264:       end).
  265: 
  266: search_wildcard(Config) ->
  267:     escalus:story(
  268:       Config, [{bob, 1}],
  269:       fun(Client) ->
  270:               Domain = domain(),
  271:               DirJID = <<"vjud.", Domain/binary>>,
  272:               Fields = [{get_field_name(fn), get_FN_wildcard()}],
  273:               Res = escalus:send_and_wait(Client,
  274:                               escalus_stanza:search_iq(DirJID,
  275:                                   escalus_stanza:search_fields(Fields))),
  276:               escalus:assert(is_iq_result, Res),
  277:               ItemTups = search_result_item_tuples(Res),
  278:               1 = length(ItemTups)
  279:       end).
  280: 
  281: %%--------------------------------------------------------------------
  282: %% Helper functions
  283: %%--------------------------------------------------------------------
  284: 
  285: expected_search_results(Key, Config) ->
  286:     {_, ExpectedResults} =
  287:     lists:keyfind(expected_results, 1,
  288:                   escalus_config:get_config(search_data, Config)),
  289:     lists:keyfind(Key, 1, ExpectedResults).
  290: 
  291: %%----------------------
  292: %% xmlel shortcuts
  293: stanza_get_vcard_field(Stanza, FieldName) ->
  294:     VCard = ?EL(Stanza, <<"vCard">>),
  295:     ?EL(VCard, FieldName).
  296: 
  297: stanza_get_vcard_field_cdata(Stanza, FieldName) ->
  298:     VCard = ?EL(Stanza, <<"vCard">>),
  299:     ?EL_CD(VCard, FieldName).
  300: 
  301: %%---------------------
  302: %% test helpers
  303: 
  304: %%
  305: %% -> [{Type, Var, Label}]
  306: %%
  307: field_tuples([]) ->
  308:     [];
  309: field_tuples([#xmlel{name = <<"field">>,
  310:                      attrs=Attrs,
  311:                      children=_Children} = El| Rest]) ->
  312:     {<<"type">>,Type} = lists:keyfind(<<"type">>, 1, Attrs),
  313:     {<<"var">>,Var} = lists:keyfind(<<"var">>, 1, Attrs),
  314:     {<<"label">>,Label} = lists:keyfind(<<"label">>, 1, Attrs),
  315:     case ?EL_CD(El, <<"value">>) of
  316:         undefined ->
  317:             [{Type, Var, Label}|field_tuples(Rest)];
  318:         ValCData ->
  319:             [{Type, Var, Label, ValCData}|field_tuples(Rest)]
  320:     end;
  321: field_tuples([_SomeOtherEl|Rest]) ->
  322:     field_tuples(Rest).
  323: 
  324: 
  325: %%
  326: %%  -> [{Type, Var, Label, ValueCData}]
  327: %%
  328: %% This is naiive and expensive LOL!
  329: item_field_tuples(_, []) ->
  330:     [];
  331: item_field_tuples(ReportedFieldTups,
  332:                   [#xmlel{name = <<"field">>,
  333:                           attrs=Attrs,
  334:                           children=_Children} = El| Rest]) ->
  335:     {<<"var">>,Var} = lists:keyfind(<<"var">>, 1, Attrs),
  336:     {Type, Var, Label} = lists:keyfind(Var, 2, ReportedFieldTups),
  337:     [{Type, Var, Label, ?EL_CD(El, <<"value">>)}
  338:      | item_field_tuples(ReportedFieldTups, Rest)];
  339: 
  340: item_field_tuples(ReportedFieldTups, [_SomeOtherEl|Rest]) ->
  341:     item_field_tuples(ReportedFieldTups, Rest).
  342: 
  343: 
  344: %%
  345: %% -> [{JID, [ItemFieldTups]}]
  346: %%
  347: %% Finds the JID and maps fields to their labels and types
  348: %%
  349: item_tuples(_, []) ->
  350:     [];
  351: item_tuples(ReportedFieldTups, [#xmlel{name = <<"item">>,
  352:                                        children = Children} | Rest]) ->
  353:     ItemFieldTups = item_field_tuples(ReportedFieldTups, Children),
  354:     {_,_,_,JID} = lists:keyfind(<<"jid">>, 2, ItemFieldTups),
  355:     [{JID, ItemFieldTups}|item_tuples(ReportedFieldTups, Rest)];
  356: item_tuples(ReportedFieldTypes, [_SomeOtherChild | Rest]) ->
  357:     item_tuples(ReportedFieldTypes, Rest).
  358: 
  359: 
  360: %% This tests that at least the values in the ExpectedVCardTups are in the
  361: %% VCardUnderTest.
  362: %% Any extra values in the vcard are ignored by this function and should be
  363: %% checked or rejected elsewhere.
  364: %% crash means fail, return means success.
  365: check_vcard(ExpectedVCardTups, Stanza) ->
  366:     escalus_pred:is_iq(<<"result">>, Stanza),
  367:     VCardUnderTest = ?EL(Stanza, <<"vCard">>),
  368:     check_xml_element(ExpectedVCardTups, VCardUnderTest).
  369: 
  370: 
  371: check_xml_element([], _ElUnderTest) ->
  372:     ok;  %% just return true to be consistent with other clauses.
  373: check_xml_element([{ExpdFieldName, ExpdChildren}|Rest], ElUnderTest)
  374:   when is_list(ExpdChildren) ->
  375:     check_xml_element(ExpdChildren, ?EL(ElUnderTest, ExpdFieldName)),
  376:     check_xml_element(Rest, ElUnderTest);
  377: check_xml_element([{ExpdFieldName, ExpdCData}|Rest], ElUnderTest) ->
  378:     case ?EL_CD(ElUnderTest, ExpdFieldName) of
  379:         ExpdCData ->
  380:             check_xml_element(Rest, ElUnderTest);
  381:         Else ->
  382:             ct:fail("Expected ~p got ~p~n", [ExpdCData, Else])
  383:     end.
  384: 
  385: %% Checks that the elements of two lists with matching keys are equal
  386: %% while the order of the elements does not matter.
  387: %% Returning means success. Crashing via ct:fail means failure.
  388: %% Prints the lists in the ct:fail Result term.
  389: list_unordered_key_match(Keypos, Expected, Actual) ->
  390:     case length(Actual) of
  391:         ActualLength when ActualLength == length(Expected) ->
  392:             list_unordered_key_match2(Keypos, Expected, Actual);
  393:         ActualLength ->
  394:             ct:fail("Expected size ~p, actual size ~p~nExpected: ~p~nActual: ~p",
  395:                     [length(Expected), ActualLength, Expected, Actual])
  396:     end.
  397: 
  398: list_unordered_key_match2(_, [], _) ->
  399:     ok;
  400: list_unordered_key_match2(Keypos, [ExpctdTup|Rest], ActualTuples) ->
  401:     Key = element(Keypos, ExpctdTup),
  402:     ActualTup = lists:keyfind(Key, Keypos, ActualTuples),
  403:     case ActualTup of
  404:         ExpctdTup ->
  405:             list_unordered_key_match2(Keypos, Rest, ActualTuples);
  406:         _ ->
  407:             ct:fail("~nExpected ~p~nGot ~p", [ExpctdTup, ActualTup])
  408:     end.
  409: 
  410: search_result_item_tuples(Stanza) ->
  411:     Result = ?EL(Stanza, <<"query">>),
  412:     XData = ?EL(Result, <<"x">>),
  413:     #xmlel{ attrs = _XAttrs,
  414:             children = XChildren } = XData,
  415:     Reported = ?EL(XData, <<"reported">>),
  416:     ReportedFieldTups = field_tuples(Reported#xmlel.children),
  417:     _ItemTups = item_tuples(ReportedFieldTups, XChildren).
  418: 
  419: get_field_name(fn)->
  420:     case is_vcard_ldap() of
  421:         true -> <<"cn">>;
  422:         false -> <<"fn">>
  423:     end;
  424: get_field_name(user)->
  425:     case is_vcard_ldap() of
  426:         true -> <<"uid">>;
  427:         false -> <<"user">>
  428:     end.
  429: 
  430: get_FN_wildcard() ->
  431:     case is_vcard_ldap() of
  432:         true -> <<"*li*e">>;
  433:         false -> <<"old*">>
  434:     end.
  435: get_FN(Config) ->
  436:     case is_vcard_ldap() of
  437:         true ->
  438:             escalus_utils:jid_to_lower(escalus_users:get_username(Config, alice));
  439:         false ->
  440:             <<"Old Name">>
  441:     end.
  442: 
  443: configure_mod_vcard(Config) ->
  444:     HostType = ct:get_config({hosts, mim, host_type}),
  445:     case is_vcard_ldap() of
  446:         true ->
  447:             ensure_started(HostType, ldap_opts());
  448:         _ ->
  449:             ensure_started(HostType, ?config(mod_vcard_opts, Config))
  450:     end.
  451: 
  452: ldap_opts() ->
  453:     LDAPOpts = #{filter => <<"(objectClass=inetOrgPerson)">>,
  454:                  base => <<"ou=Users,dc=esl,dc=com">>,
  455:                  search_fields => [{<<"Full Name">>, <<"cn">>}, {<<"User">>, <<"uid">>}],
  456:                  vcard_map => [{<<"FN">>, <<"%s">>, [<<"cn">>]}]},
  457:     LDAPOptsWithDefaults = config_parser_helper:config([modules, mod_vcard, ldap], LDAPOpts),
  458:     config_parser_helper:mod_config(mod_vcard, #{backend => ldap, ldap => LDAPOptsWithDefaults}).
  459: 
  460: ensure_started(HostType, Opts) ->
  461:     dynamic_modules:stop(HostType, mod_vcard),
  462:     dynamic_modules:start(HostType, mod_vcard, Opts).
  463: 
  464: prepare_vcard_module(Config) ->
  465:     %% Keep the old config, so we can undo our changes, once finished testing
  466:     Config1 = dynamic_modules:save_modules(host_types(), Config),
  467:     %% Get a list of options, we can use as a prototype to start new modules
  468:     Backend = mongoose_helper:mnesia_or_rdbms_backend(),
  469:     VCardOpts = config_parser_helper:mod_config(mod_vcard, #{backend => Backend}),
  470:     [{mod_vcard_opts, VCardOpts} | Config1].
  471: 
  472: restore_vcard_module(Config) ->
  473:     dynamic_modules:restore_modules(Config).