1: %%==============================================================================
    2: %% Copyright 2012 Erlang Solutions Ltd.
    3: %%
    4: %% Test the mod_vcard* modules.
    5: %% mod_vcard uses mnesia
    6: %% mod_vcard_ldap uses ldap
    7: %% mod_vcard_rdbms uses rdbms
    8: %%
    9: %% They share many comonalities but sometimes behave differently or have
   10: %% some extra or missing features. They also need different config depending
   11: %% on which vhost they're running on.
   12: %%
   13: %%           -----
   14: %%
   15: %% Licensed under the Apache License, Version 2.0 (the "License");
   16: %% you may not use this file except in compliance with the License.
   17: %% You may obtain a copy of the License at
   18: %%
   19: %% http://www.apache.org/licenses/LICENSE-2.0
   20: %%
   21: %% Unless required by applicable law or agreed to in writing, software
   22: %% distributed under the License is distributed on an "AS IS" BASIS,
   23: %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   24: %% See the License for the specific language governing permissions and
   25: %% limitations under the License.
   26: %%==============================================================================
   27: 
   28: -module(vcard_SUITE).
   29: -compile([export_all, nowarn_export_all]).
   30: 
   31: -include_lib("common_test/include/ct.hrl").
   32: -include_lib("escalus/include/escalus_xmlns.hrl").
   33: -include_lib("escalus/include/escalus.hrl").
   34: -include_lib("exml/include/exml.hrl").
   35: 
   36: %% Element CData
   37: -define(EL(Element, Name), exml_query:path(Element, [{element, Name}])).
   38: -define(EL_CD(Element, Name), exml_query:path(Element, [{element, Name}, cdata])).
   39: 
   40: -define(PHOTO_BIN, <<130, 192, 33, 159, 204, 86, 12, 63, 132, 164>>).
   41: -define(PHOTO_BASE_64, <<"gsAhn8xWDD+EpA==">>). %% jlib:encode_base64(?PHOTO_BIN)
   42: 
   43: -import(distributed_helper, [mim/0,
   44:                              require_rpc_nodes/1,
   45:                              subhost_pattern/1,
   46:                              rpc/4]).
   47: -import(ldap_helper, [get_ldap_base/1,
   48:                       call_ldap/3]).
   49: -import(domain_helper, [host_type/0,
   50:                         host_types/0,
   51:                         domain/0]).
   52: 
   53: %%--------------------------------------------------------------------
   54: %% Suite configuration
   55: %%--------------------------------------------------------------------
   56: 
   57: all() ->
   58:     [{group, rw},
   59:      {group, ro_full},
   60:      {group, ro_limited},
   61:      {group, params_limited_infinity},
   62:      {group, ro_no},
   63:      {group, ldap_only}
   64:      ].
   65: 
   66: groups() ->
   67:     %% setting test data before tests is proving awkward so might as well use the
   68:     %% data set in the update tests to test the rest.
   69:     [{rw, [sequence], rw_tests()},
   70:      {ro_full, [], ro_full_search_tests()},
   71:      {ro_limited, [], ro_limited_search_tests()},
   72:      {params_limited_infinity, [], rw_tests()},
   73:      {ro_no, [sequence], ro_no_search_tests()},
   74:      {ldap_only, [], ldap_only_tests()}].
   75: 
   76: rw_tests() ->
   77:     [
   78:      update_own_card,
   79:      cant_update_own_card_with_invalid_field,
   80:      can_update_own_card_with_emoji_in_nickname,
   81:      can_update_own_card_with_unicode_in_address
   82:     ].
   83: 
   84: ro_full_search_tests() ->
   85:     [retrieve_own_card,
   86:      user_doesnt_exist,
   87:      update_other_card,
   88:      retrieve_others_card,
   89:      vcard_service_discovery,
   90:      server_vcard,
   91:      directory_service_vcard,
   92:      request_search_fields,
   93:      search_open,
   94:      search_empty,
   95:      search_some,
   96:      search_some_many_fields,
   97:      search_wildcard,
   98:      search_rsm_count,
   99:      search_rsm_forward,
  100:      search_rsm_backward,
  101:      search_rsm_pages,
  102:      search_errors
  103:     ].
  104: 
  105: ro_limited_search_tests() ->
  106:     [search_open_limited,
  107:      search_some_limited,
  108:      search_in_service_discovery].
  109: 
  110: ro_no_search_tests() ->
  111:     [search_not_allowed,
  112:      search_not_in_service_discovery].
  113: 
  114: ldap_only_tests() ->
  115:     [search_some_many_fields_with_or_operator,
  116:      user_doesnt_exist,
  117:      return_photo_inserted_as_binary_by_3rd_party_service].
  118: 
  119: suite() ->
  120:     require_rpc_nodes([mim]) ++ escalus:suite().
  121: 
  122: %%--------------------------------------------------------------------
  123: %% Init & teardown
  124: %%--------------------------------------------------------------------
  125: 
  126: init_per_suite(Config) ->
  127:     NewConfig = escalus:init_per_suite(Config),
  128:     NewConfig1 = vcard_config(NewConfig),
  129:     NewConfig2 = prepare_vcard_module(NewConfig1),
  130:     AliceAndBob = escalus_users:get_users([alice, bob]),
  131:     Spec = escalus_users:get_userspec(Config, alice),
  132:     Addr = proplists:get_value(host, Spec, <<"localhost">>),
  133:     SecDomain = secondary_domain(),
  134:     BisUsers = [{aliceb, [{username, <<"aliceb">>},
  135:                           {server, SecDomain},
  136:                           {host, Addr},
  137:                           {password, <<"makota">>}]},
  138:                 {bobb, [{username, <<"bobb">>},
  139:                         {server, SecDomain},
  140:                         {host, Addr},
  141:                         {password, <<"makrolika">>}]}],
  142:     NewUsers = AliceAndBob ++ BisUsers,
  143:     escalus:create_users([{escalus_users, NewUsers} | NewConfig2], NewUsers).
  144: 
  145: vcard_config(Config) ->
  146:     Domain = domain(),
  147:     [{all_vcards, get_all_vcards()},
  148:      {server_vcards, get_server_vcards()},
  149:      {directory_jid, <<"vjud.", Domain/binary>>}
  150:     ] ++ Config.
  151: 
  152: end_per_suite(Config) ->
  153:     restore_vcard_module(Config),
  154:     Who = ?config(escalus_users, Config),
  155:     NewConfig = escalus:delete_users(Config, Who),
  156:     escalus:end_per_suite(NewConfig).
  157: 
  158: init_per_group(Group, Config) when Group == rw; Group == params_limited_infinity ->
  159:     restart_vcard_mod(Config, Group),
  160:     case vcard_helper:is_vcard_ldap() of
  161:         true ->
  162:             {skip, ldap_vcard_is_readonly};
  163:         _ ->
  164:             Config
  165:     end;
  166: init_per_group(ldap_only, Config) ->
  167:     VCardConfig = ?config(mod_vcard_opts, Config),
  168:     case maps:get(backend, VCardConfig) of
  169:         ldap ->
  170:             Config1 = restart_and_prepare_vcard(ldap_only, Config),
  171:             insert_alice_photo(Config1);
  172:         _ ->
  173:             {skip, "this group is only for ldap vCard backend"}
  174:     end;
  175: init_per_group(GroupName, Config) ->
  176:     restart_and_prepare_vcard(GroupName, Config).
  177: 
  178: end_per_group(_, Config) ->
  179:     stop_vcard_mod(Config),
  180:     Config.
  181: 
  182: init_per_testcase(CaseName, Config) ->
  183:     escalus:init_per_testcase(CaseName, Config).
  184: 
  185: end_per_testcase(CaseName, Config) ->
  186:     escalus:end_per_testcase(CaseName, Config).
  187: 
  188: restart_and_prepare_vcard(GroupName, Config) ->
  189:     restart_vcard_mod(Config, GroupName),
  190:     prepare_vcards(Config).
  191: 
  192: %%--------------------------------------------------------------------
  193: %% XEP-0054: vcard-temp Test cases
  194: %%--------------------------------------------------------------------
  195: 
  196: update_own_card(Config) ->
  197:     escalus:story(
  198:         Config, [{alice, 1}],
  199:         fun(Client1) ->
  200:                 %% set some initial value different from the actual test data
  201:                 %% so we know it really got updated and wasn't just old data
  202:                 Client1Fields = [{<<"FN">>, <<"Old name">>}],
  203:                 Client1SetResultStanza
  204:                     = escalus:send_and_wait(Client1,
  205:                                         escalus_stanza:vcard_update(Client1Fields)),
  206:                 escalus:assert(is_iq_result, Client1SetResultStanza),
  207:                 Client1GetResultStanza
  208:                     = escalus:send_and_wait(Client1, escalus_stanza:vcard_request()),
  209:                 <<"Old name">>
  210:                     = stanza_get_vcard_field_cdata(Client1GetResultStanza, <<"FN">>)
  211:         end).
  212: 
  213: cant_update_own_card_with_invalid_field(Config) ->
  214:     escalus:story(
  215:         Config, [{alice, 1}],
  216:         fun(Client1) ->
  217:                 %% One of "Non-character code points". Banned in stringrep but still valid UTF.
  218:                 Client1Fields = [{<<"FN">>, <<243,191,191,190>>}],
  219:                 Client1SetResultStanza
  220:                     = escalus:send_and_wait(Client1,
  221:                                         escalus_stanza:vcard_update(Client1Fields)),
  222:                 escalus:assert(is_iq_error, Client1SetResultStanza)
  223:         end).
  224: 
  225: can_update_own_card_with_emoji_in_nickname(Config) ->
  226:     escalus:story(
  227:         Config, [{alice, 1}],
  228:         fun(Client1) ->
  229:                 NickWithEmoji = <<"Jan 😊"/utf8>>,
  230:                 Client1Fields = [{<<"NICKNAME">>, NickWithEmoji}],
  231:                 Client1SetResultStanza
  232:                     = escalus:send_and_wait(Client1,
  233:                                             escalus_stanza:vcard_update(Client1Fields)),
  234:                 escalus:assert(is_iq_result, Client1SetResultStanza),
  235:                 Client1GetResultStanza
  236:                     = escalus:send_and_wait(Client1, escalus_stanza:vcard_request()),
  237:                 NickWithEmoji = stanza_get_vcard_field_cdata(Client1GetResultStanza, <<"NICKNAME">>)
  238:         end).
  239: 
  240: can_update_own_card_with_unicode_in_address(Config) ->
  241:     escalus:story(
  242:         Config, [{alice, 1}],
  243:         fun(Client1) ->
  244:                 Locality = get_utf8_city(),
  245:                 Client1Fields = [{<<"ADDR">>, [{<<"LOCALITY">>, get_utf8_city()}]}],
  246:                 Client1SetResultStanza
  247:                     = escalus:send_and_wait(Client1,
  248:                                             escalus_stanza:vcard_update(Client1Fields)),
  249:                 escalus:assert(is_iq_result, Client1SetResultStanza),
  250:                 Client1GetResultStanza
  251:                     = escalus:send_and_wait(Client1, escalus_stanza:vcard_request()),
  252:                 Addr = stanza_get_vcard_field(Client1GetResultStanza, <<"ADDR">>),
  253:                 Locality = ?EL_CD(Addr, <<"LOCALITY">>)
  254:         end).
  255: 
  256: retrieve_own_card(Config) ->
  257:     escalus:story(
  258:       Config, [{alice, 1}],
  259:       fun(Client) ->
  260:               Res = escalus:send_and_wait(Client,
  261:                         escalus_stanza:vcard_request()),
  262:               JID = escalus_utils:jid_to_lower(escalus_client:short_jid(Client)),
  263:               ClientVCardTups = get_user_vcard(JID, Config),
  264:               check_vcard(ClientVCardTups, Res)
  265:       end).
  266: 
  267: 
  268: 
  269: %% If no vCard exists, the server MUST return a stanza error
  270: %% (which SHOULD be <item-not-found/>) or an IQ-result
  271: %% containing an empty <vCard/> element.
  272: %% We return <item-not-found/>
  273: user_doesnt_exist(Config) ->
  274:     escalus:story(
  275:       Config, [{alice, 1}],
  276:       fun(Client) ->
  277:               Domain = domain(),
  278:               BadJID = <<"nonexistent@", Domain/binary>>,
  279:               Res = escalus:send_and_wait(Client,
  280:                         escalus_stanza:vcard_request(BadJID)),
  281:           case
  282:           escalus_pred:is_error(<<"cancel">>,
  283:               <<"item-not-found">>,
  284:               Res) of
  285:               true ->
  286:                   ok;
  287:               _ ->
  288:                   [] = Res#xmlel.children,
  289:                   ct:comment("empty result instead of error")
  290:           end
  291:       end).
  292: 
  293: update_other_card(Config) ->
  294:     escalus:story(
  295:       Config, [{alice, 1}, {bob, 1}],
  296:       fun(Client, OtherClient) ->
  297:               JID = escalus_utils:jid_to_lower(escalus_client:short_jid(Client)),
  298:               Fields = [{<<"FN">>, <<"New name">>}],
  299:               Res = escalus:send_and_wait(OtherClient,
  300:                         escalus_stanza:vcard_update(JID, Fields)),
  301: 
  302:               %% auth forbidden is also allowed
  303:               escalus:assert(is_error, [<<"cancel">>,
  304:                                         <<"not-allowed">>], Res),
  305: 
  306:               %% check that nothing was changed
  307:               Res2 = escalus:send_and_wait(Client,
  308:                         escalus_stanza:vcard_request()),
  309:               ClientVCardTups = get_user_vcard(JID, Config),
  310:               check_vcard(ClientVCardTups, Res2)
  311:       end).
  312: 
  313: retrieve_others_card(Config) ->
  314:     escalus:story(
  315:       Config, [{alice, 1}, {bob, 1}],
  316:       fun(Client, OtherClient) ->
  317:               OtherJID = escalus_utils:jid_to_lower(escalus_client:short_jid(OtherClient)),
  318:               Res = escalus:send_and_wait(Client,
  319:                         escalus_stanza:vcard_request(OtherJID)),
  320:               OtherClientVCardTups = get_user_vcard(OtherJID, Config),
  321:               check_vcard(OtherClientVCardTups, Res),
  322: 
  323:               %% In accordance with XMPP Core [5], a compliant server MUST
  324:               %% respond on behalf of the requestor and not forward the IQ to
  325:               %% the requestee's connected resource.
  326: 
  327:               Res2 = (catch escalus:wait_for_stanza(OtherClient)),
  328:               escalus:assert(stanza_timeout, Res2)
  329:       end).
  330: 
  331: server_vcard(Config) ->
  332:     escalus:story(
  333:       Config, [{alice, 1}],
  334:       fun(Client) ->
  335:               Domain = domain(),
  336:               Res = escalus:send_and_wait(Client,
  337:                         escalus_stanza:vcard_request(Domain)),
  338:               ServerVCardTups = get_server_vcard(Domain, Config),
  339:               check_vcard(ServerVCardTups, Res)
  340:       end).
  341: 
  342: directory_service_vcard(Config) ->
  343:     escalus:story(
  344:       Config, [{alice, 1}],
  345:       fun(Client) ->
  346:               DirJID = ?config(directory_jid, Config),
  347:               Res = escalus:send_and_wait(Client,
  348:                         escalus_stanza:vcard_request(DirJID)),
  349:               DirVCardTups = get_server_vcard(DirJID, Config),
  350:               check_vcard(DirVCardTups, Res)
  351:       end).
  352: 
  353: vcard_service_discovery(Config) ->
  354:     escalus:story(
  355:       Config, [{alice, 1}],
  356:       fun(Client) ->
  357:           Domain = domain(),
  358:               Res = escalus:send_and_wait(Client,
  359:                         escalus_stanza:disco_info(Domain)),
  360:               escalus:assert(is_iq_result, Res),
  361:               escalus:assert(has_feature, [<<"vcard-temp">>], Res)
  362:     end).
  363: 
  364: %%--------------------------------------------------------------------
  365: %% XEP-0055 jabber:iq:search User Directory service Test cases
  366: %%
  367: %%--------------------------------------------------------------------
  368: 
  369: %% all.search.domain
  370: 
  371: request_search_fields(Config) ->
  372:     escalus:story(
  373:       Config, [{alice, 1}],
  374:       fun(Client) ->
  375:               DirJID = ?config(directory_jid, Config),
  376:               Res = escalus:send_and_wait(Client,
  377:                         escalus_stanza:search_fields_iq(DirJID)),
  378:               escalus:assert(is_iq_result, Res),
  379:               Result = ?EL(Res, <<"query">>),
  380:               XData = ?EL(Result, <<"x">>),
  381:               #xmlel{ children = XChildren } = XData,
  382:               FieldTups = field_tuples(XChildren),
  383:               true = lists:member({<<"text-single">>,
  384:                                    get_user_search_field(), <<"User">>},
  385:                                   FieldTups),
  386:               true = lists:member({<<"text-single">>,
  387:                                    get_full_name_search_field(),
  388:                                    <<"Full Name">>},
  389:                                   FieldTups)
  390:       end).
  391: 
  392: search_open(Config) ->
  393:     escalus:story(
  394:       Config, [{alice, 1}],
  395:       fun(Client) ->
  396:               DirJID = ?config(directory_jid, Config),
  397:               Fields = [#xmlel{ name = <<"field">> }],
  398:               Res = escalus:send_and_wait(Client,
  399:                         escalus_stanza:search_iq(DirJID, Fields)),
  400:               escalus:assert(is_iq_result, Res),
  401: 
  402:               %% Basically test that the right values exist
  403:               %% and map to the right column headings
  404:               ItemTups = search_result_item_tuples(Res),
  405:               ExpectedItemTups = [],
  406:               list_unordered_key_match(ExpectedItemTups, ItemTups)
  407:       end).
  408: 
  409: search_empty(Config) ->
  410:     escalus:story(
  411:       Config, [{alice, 1}],
  412:       fun(Client) ->
  413:               DirJID = ?config(directory_jid, Config),
  414:               Fields = [{<<"fn">>, <<"nobody">>}],
  415:               Res = escalus:send_and_wait(Client,
  416:                         escalus_stanza:search_iq(DirJID,
  417:                             escalus_stanza:search_fields(Fields))),
  418:               escalus:assert(is_iq_result, Res),
  419:               [] = search_result_item_tuples(Res)
  420:       end).
  421: 
  422: search_some(Config) ->
  423:     escalus:story(
  424:       Config, [{bob, 1}],
  425:       fun(Client) ->
  426:               DirJID = ?config(directory_jid, Config),
  427:               MoscowRUBin = get_utf8_city(),
  428:               Fields = [{get_locality_search_field(), MoscowRUBin}],
  429:               Res = escalus:send_and_wait(Client,
  430:                         escalus_stanza:search_iq(DirJID,
  431:                             escalus_stanza:search_fields(Fields))),
  432:               escalus:assert(is_iq_result, Res),
  433: 
  434:               %% Basically test that the right values exist
  435:               %% and map to the right column headings
  436:               Domain = domain(),
  437:               AliceJID = <<"alice@", Domain/binary>>,
  438: 
  439:               %% Extra check
  440:               _Result = escalus:send_and_wait(Client, escalus_stanza:vcard_request(AliceJID)),
  441: 
  442:               [{AliceJID, ItemTups}] = search_result_item_tuples(Res),
  443:               {_, _, <<"City">>, MoscowRUBin} = lists:keyfind(<<"City">>, 3, ItemTups)
  444:       end).
  445: 
  446: search_wildcard(Config) ->
  447:     escalus:story(
  448:         Config, [{bob, 1}],
  449:         fun(Client) ->
  450:                 Domain = ct:get_config({hosts, mim, secondary_domain}),
  451:                 DirJID = <<"vjud.", Domain/binary>>,
  452:                 Fields = [{get_full_name_search_field(),
  453:                            <<"Doe*">>}],
  454:                 Res = escalus:send_and_wait(Client,
  455:                         escalus_stanza:search_iq(DirJID,
  456:                             escalus_stanza:search_fields(Fields))),
  457:                 escalus:assert(is_iq_result, Res),
  458: 
  459:                 ItemTups = search_result_item_tuples(Res),
  460:                 ExpectedItemTups = get_search_results(Config,
  461:                                                       [<<"bobb@", Domain/binary>>,
  462:                                                        <<"aliceb@", Domain/binary>>]),
  463:                 case vcard_helper:is_vcard_ldap() of
  464:                     true ->
  465:                         3 = length(ItemTups);
  466:                     _ ->
  467:                         list_unordered_key_match2(ExpectedItemTups, ItemTups)
  468:                 end
  469:         end).
  470: 
  471: search_rsm_pages(Config) ->
  472:     escalus:story(
  473:         Config, [{bob, 1}],
  474:         fun(Client) ->
  475:                 Domain = ct:get_config({hosts, mim, secondary_domain}),
  476:                 DirJID = <<"vjud.", Domain/binary>>,
  477:                 Fields = [{get_full_name_search_field(),
  478:                            <<"Doe*">>}],
  479:                 Iq1 = escalus_stanza:search_iq(
  480:                         DirJID,
  481:                         escalus_stanza:search_fields(Fields)
  482:                        ),
  483:                 Iq2 = append_to_query(
  484:                         Iq1,
  485:                         #xmlel{name = <<"set">>,
  486:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  487:                                children =
  488:                                    [#xmlel{name = "max",
  489:                                            children =
  490:                                                [#xmlcdata{content = <<"1">>}]
  491:                                           }
  492:                                    ]}
  493:                        ),
  494: 
  495:                 Res1 = escalus:send_and_wait(Client, Iq2),
  496:                 escalus:assert(is_iq_result, Res1),
  497: 
  498:                 RSMCount1 = get_rsm_count(Res1),
  499:                 case vcard_helper:is_vcard_ldap() of
  500:                     true ->
  501:                         <<"3">> = RSMCount1;
  502:                     false ->
  503:                         <<"2">> = RSMCount1
  504:                 end,
  505:                 ItemTups1 = search_result_item_tuples(Res1),
  506: 
  507:                 Iq3 = append_to_query(
  508:                         Iq1,
  509:                         #xmlel{name = <<"set">>,
  510:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  511:                                children =
  512:                                    [#xmlel{name = "max",
  513:                                            children =
  514:                                                [#xmlcdata{content = <<"1">>}]
  515:                                           },
  516:                                     #xmlel{name = "index",
  517:                                            children =
  518:                                                [#xmlcdata{content = <<"1">>}]
  519:                                           }
  520:                                    ]}
  521:                        ),
  522: 
  523:                 Res2 = escalus:send_and_wait(Client, Iq3),
  524:                 escalus:assert(is_iq_result, Res2),
  525: 
  526:                 RSMCount2 = get_rsm_count(Res2),
  527:                 case vcard_helper:is_vcard_ldap() of
  528:                     true ->
  529:                         <<"3">> = RSMCount2;
  530:                     false ->
  531:                         <<"2">> = RSMCount2
  532:                 end,
  533:                 ItemTups2 = search_result_item_tuples(Res2),
  534:                 SecDomain = secondary_domain(),
  535:                 ExpectedItemTups = get_search_results(
  536:                                      Config,
  537:                                      [<<"bobb@", SecDomain/binary>>,
  538:                                       <<"aliceb@", SecDomain/binary>>]),
  539:                 case vcard_helper:is_vcard_ldap() of
  540:                     true ->
  541:                         ignore;
  542:                     _ ->
  543:                         list_unordered_key_match2(ExpectedItemTups,
  544:                                                   ItemTups1 ++ ItemTups2)
  545:                 end
  546:         end).
  547: 
  548: search_rsm_forward(Config) ->
  549:     escalus:story(
  550:         Config, [{bob, 1}],
  551:         fun(Client) ->
  552:                 Domain = ct:get_config({hosts, mim, secondary_domain}),
  553:                 DirJID = <<"vjud.", Domain/binary>>,
  554:                 Fields = [{get_full_name_search_field(),
  555:                            <<"Doe*">>}],
  556:                 Iq1 = escalus_stanza:search_iq(
  557:                         DirJID,
  558:                         escalus_stanza:search_fields(Fields)
  559:                        ),
  560:                 Iq2 = append_to_query(
  561:                         Iq1,
  562:                         #xmlel{name = <<"set">>,
  563:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  564:                                children =
  565:                                    [#xmlel{name = "max",
  566:                                            children =
  567:                                                [#xmlcdata{content = <<"1">>}]
  568:                                           }
  569:                                    ]}
  570:                        ),
  571: 
  572:                 Res1 = escalus:send_and_wait(Client, Iq2),
  573:                 escalus:assert(is_iq_result, Res1),
  574: 
  575:                 RSMCount1 = get_rsm_count(Res1),
  576:                 case vcard_helper:is_vcard_ldap() of
  577:                     true ->
  578:                         <<"3">> = RSMCount1;
  579:                     false ->
  580:                         <<"2">> = RSMCount1
  581:                 end,
  582:                 RSMLast1 = get_rsm_last(Res1),
  583:                 ItemTups1 = search_result_item_tuples(Res1),
  584: 
  585:                 Iq3 = append_to_query(
  586:                         Iq1,
  587:                         #xmlel{name = <<"set">>,
  588:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  589:                                children =
  590:                                    [#xmlel{name = "max",
  591:                                            children =
  592:                                                [#xmlcdata{content = <<"1">>}]
  593:                                           },
  594:                                     #xmlel{name = "after",
  595:                                            children =
  596:                                                [#xmlcdata{content = RSMLast1}]
  597:                                           }
  598:                                    ]}
  599:                        ),
  600: 
  601:                 Res2 = escalus:send_and_wait(Client, Iq3),
  602:                 escalus:assert(is_iq_result, Res2),
  603: 
  604:                 RSMCount2 = get_rsm_count(Res2),
  605:                 case vcard_helper:is_vcard_ldap() of
  606:                     true ->
  607:                         <<"3">> = RSMCount2;
  608:                     false ->
  609:                         <<"2">> = RSMCount2
  610:                 end,
  611:                 RSMLast2 = get_rsm_last(Res2),
  612:                 ItemTups2 = search_result_item_tuples(Res2),
  613:                 SecDomain = secondary_domain(),
  614:                 ExpectedItemTups = get_search_results(
  615:                                      Config,
  616:                                      [<<"bobb@", SecDomain/binary>>,
  617:                                       <<"aliceb@", SecDomain/binary>>]),
  618:                 case vcard_helper:is_vcard_ldap() of
  619:                     true ->
  620:                         ignore;
  621:                     _ ->
  622:                         list_unordered_key_match2(ExpectedItemTups,
  623:                                                   ItemTups1 ++ ItemTups2)
  624:                 end,
  625: 
  626:                 Iq4 = append_to_query(
  627:                         Iq1,
  628:                         #xmlel{name = <<"set">>,
  629:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  630:                                children =
  631:                                    [#xmlel{name = "max",
  632:                                            children =
  633:                                                [#xmlcdata{content = <<"1">>}]
  634:                                           },
  635:                                     #xmlel{name = "after",
  636:                                            children =
  637:                                                [#xmlcdata{content = RSMLast2}]
  638:                                           }
  639:                                    ]}
  640:                        ),
  641: 
  642:                 Res3 = escalus:send_and_wait(Client, Iq4),
  643:                 escalus:assert(is_iq_result, Res3),
  644: 
  645:                 RSMCount2 = get_rsm_count(Res3),
  646:                 case vcard_helper:is_vcard_ldap() of
  647:                     true ->
  648:                         <<"3">> = RSMCount2,
  649:                         [_] = search_result_item_tuples(Res3);
  650:                     false ->
  651:                         <<"2">> = RSMCount2,
  652:                         [] = search_result_item_tuples(Res3)
  653:                 end
  654:         end).
  655: 
  656: search_rsm_backward(Config) ->
  657:     escalus:story(
  658:         Config, [{bob, 1}],
  659:         fun(Client) ->
  660:                 Domain = ct:get_config({hosts, mim, secondary_domain}),
  661:                 DirJID = <<"vjud.", Domain/binary>>,
  662:                 Fields = [{get_full_name_search_field(),
  663:                            <<"Doe*">>}],
  664:                 Iq1 = escalus_stanza:search_iq(
  665:                         DirJID,
  666:                         escalus_stanza:search_fields(Fields)
  667:                        ),
  668:                 Iq2 = append_to_query(
  669:                         Iq1,
  670:                         #xmlel{name = <<"set">>,
  671:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  672:                                children =
  673:                                    [#xmlel{name = "max",
  674:                                            children =
  675:                                                [#xmlcdata{content = <<"1">>}]
  676:                                           },
  677:                                     #xmlel{name = "before"}
  678:                                    ]}
  679:                        ),
  680: 
  681:                 Res1 = escalus:send_and_wait(Client, Iq2),
  682:                 escalus:assert(is_iq_result, Res1),
  683: 
  684:                 RSMCount1 = get_rsm_count(Res1),
  685:                 case vcard_helper:is_vcard_ldap() of
  686:                     true ->
  687:                         <<"3">> = RSMCount1;
  688:                     false ->
  689:                         <<"2">> = RSMCount1
  690:                 end,
  691:                 RSMFirst1 = get_rsm_first(Res1),
  692:                 ItemTups1 = search_result_item_tuples(Res1),
  693: 
  694:                 Iq3 = append_to_query(
  695:                         Iq1,
  696:                         #xmlel{name = <<"set">>,
  697:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  698:                                children =
  699:                                    [#xmlel{name = "max",
  700:                                            children =
  701:                                                [#xmlcdata{content = <<"1">>}]
  702:                                           },
  703:                                     #xmlel{name = "before",
  704:                                            children =
  705:                                                [#xmlcdata{content = RSMFirst1}]
  706:                                           }
  707:                                    ]}
  708:                        ),
  709: 
  710:                 Res2 = escalus:send_and_wait(Client, Iq3),
  711:                 escalus:assert(is_iq_result, Res2),
  712: 
  713:                 RSMCount2 = get_rsm_count(Res2),
  714:                 case vcard_helper:is_vcard_ldap() of
  715:                     true ->
  716:                         <<"3">> = RSMCount2;
  717:                     false ->
  718:                         <<"2">> = RSMCount2
  719:                 end,
  720:                 RSMFirst2 = get_rsm_first(Res2),
  721:                 ItemTups2 = search_result_item_tuples(Res2),
  722:                 SecDomain = secondary_domain(),
  723:                 ExpectedItemTups = get_search_results(
  724:                                      Config,
  725:                                      [<<"bobb@", SecDomain/binary>>,
  726:                                       <<"aliceb@", SecDomain/binary>>]),
  727:                 case vcard_helper:is_vcard_ldap() of
  728:                     true ->
  729:                         ignore;
  730:                     _ ->
  731:                         list_unordered_key_match2(ExpectedItemTups,
  732:                                                   ItemTups1 ++ ItemTups2)
  733:                 end,
  734: 
  735:                 Iq4 = append_to_query(
  736:                         Iq1,
  737:                         #xmlel{name = <<"set">>,
  738:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  739:                                children =
  740:                                    [#xmlel{name = "max",
  741:                                            children =
  742:                                                [#xmlcdata{content = <<"1">>}]
  743:                                           },
  744:                                     #xmlel{name = "before",
  745:                                            children =
  746:                                                [#xmlcdata{content = RSMFirst2}]
  747:                                           }
  748:                                    ]}
  749:                        ),
  750: 
  751:                 Res3 = escalus:send_and_wait(Client, Iq4),
  752:                 escalus:assert(is_iq_result, Res3),
  753: 
  754:                 RSMCount2 = get_rsm_count(Res3),
  755:                 case vcard_helper:is_vcard_ldap() of
  756:                     true ->
  757:                         <<"3">> = RSMCount2,
  758:                         [_] = search_result_item_tuples(Res3);
  759:                     false ->
  760:                         <<"2">> = RSMCount2,
  761:                         [] = search_result_item_tuples(Res3)
  762:                 end
  763:         end).
  764: 
  765: search_rsm_count(Config) ->
  766:     escalus:story(
  767:         Config, [{bob, 1}],
  768:         fun(Client) ->
  769:                 Domain = ct:get_config({hosts, mim, secondary_domain}),
  770:                 DirJID = <<"vjud.", Domain/binary>>,
  771:                 Fields = [{get_full_name_search_field(),
  772:                            <<"Doe*">>}],
  773:                 Iq1 = escalus_stanza:search_iq(
  774:                         DirJID,
  775:                         escalus_stanza:search_fields(Fields)
  776:                        ),
  777:                 Iq2 = append_to_query(
  778:                         Iq1,
  779:                         #xmlel{name = <<"set">>,
  780:                                attrs = [{<<"xmlns">>, ?NS_RSM}],
  781:                                children =
  782:                                    [#xmlel{name = "max",
  783:                                            children =
  784:                                                [#xmlcdata{content = <<"0">>}]
  785:                                           }
  786:                                    ]}
  787:                        ),
  788: 
  789:                 Res = escalus:send_and_wait(Client, Iq2),
  790:                 escalus:assert(is_iq_result, Res),
  791: 
  792:                 ItemTups = search_result_item_tuples(Res),
  793:                 0 = length(ItemTups),
  794: 
  795:                 RSMCount = get_rsm_count(Res),
  796:                 case vcard_helper:is_vcard_ldap() of
  797:                     true ->
  798:                         <<"3">> = RSMCount;
  799:                     false ->
  800:                         <<"2">> = RSMCount
  801:                 end
  802:         end).
  803: 
  804: search_errors(Config) ->
  805:     escalus:story(
  806:       Config, [{alice, 1}],
  807:       fun(Client) ->
  808:               DirJID = ?config(directory_jid, Config),
  809:               Fields = [#xmlel{ name = <<"field">> }],
  810:               Req = escalus_stanza:search_iq(DirJID, Fields),
  811: 
  812:               escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  813:                              escalus:send_and_wait(Client, form_helper:remove_form_types(Req))),
  814:               escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  815:                              escalus:send_and_wait(Client, form_helper:remove_form_ns(Req))),
  816:               escalus:assert(is_error, [<<"modify">>, <<"bad-request">>],
  817:                              escalus:send_and_wait(Client, form_helper:remove_forms(Req)))
  818:       end).
  819: 
  820: get_rsm_count(El) ->
  821:     exml_query:path(El, [{element, <<"query">>},
  822:                          {element, <<"set">>},
  823:                          {element, <<"count">>},
  824:                          cdata]).
  825: 
  826: get_rsm_first(El) ->
  827:     exml_query:path(El, [{element, <<"query">>},
  828:                          {element, <<"set">>},
  829:                          {element, <<"first">>},
  830:                          cdata]).
  831: 
  832: get_rsm_last(El) ->
  833:     exml_query:path(El, [{element, <<"query">>},
  834:                          {element, <<"set">>},
  835:                          {element, <<"last">>},
  836:                          cdata]).
  837: 
  838: append_to_query(#xmlel{name = <<"iq">>,
  839:                        children = IqChildren} = Iq,
  840:                 NewChild) ->
  841:     Iq#xmlel{
  842:       children =
  843:           lists:map(
  844:             fun(#xmlel{
  845:                    name = <<"query">>,
  846:                    children = QueryChildren
  847:                   } = Query) ->
  848:                     Query#xmlel{children = QueryChildren ++ [NewChild]};
  849:                (El) ->
  850:                     El
  851:             end, IqChildren)}.
  852: 
  853: search_some_many_fields(Config) ->
  854:     escalus:story(
  855:       Config, [{alice, 1}],
  856:       fun(Client) ->
  857:               {Domain, ItemTups} = search_vcard_by_many_fields(Client),
  858:               SomeJID = <<"aliceb@", Domain/binary>>,
  859:               [{SomeJID, _JIDsFields}] = ItemTups
  860:               %{_Start, _Length} = binary:match(SomeJID, <<"@", Server/binary>>)
  861:       end).
  862: 
  863: search_vcard_by_many_fields(Client) ->
  864:     Domain = ct:get_config({hosts, mim, secondary_domain}),
  865:     DirJID = <<"vjud.", Domain/binary>>,
  866: 
  867:     Fields = [{get_first_name_search_field(), <<"Alice">>},
  868:               {get_last_name_search_field(), <<"Doe">>}],
  869:     Stanza = escalus_stanza:search_iq(DirJID,
  870:                                       escalus_stanza:search_fields(Fields)),
  871:     Res = escalus:send_and_wait(Client, Stanza),
  872:     escalus:assert(is_iq_result, Res),
  873:     ItemTups = search_result_item_tuples(Res),
  874:     %% exactly one result returned and its JID domain is correct
  875:     {Domain, ItemTups}.
  876: 
  877: search_some_many_fields_with_or_operator(Config) ->
  878:     escalus:story(Config, [{alice, 1}], fun(Client) ->
  879:         {_Domain, ItemTups} = search_vcard_by_many_fields(Client),
  880:         3 = length(ItemTups)
  881:     end).
  882: 
  883: return_photo_inserted_as_binary_by_3rd_party_service(Config) ->
  884:     %% The PHOTO was inserted by script and not by modifing vCard by the client
  885:     escalus:story(Config, [{alice, 1}], fun(Alice) ->
  886:         Res = escalus:send_and_wait(Alice,
  887:                                     escalus_stanza:vcard_request()),
  888:         JID = escalus_utils:jid_to_lower(escalus_client:short_jid(Alice)),
  889: 
  890:         Photo = exml_query:path(Res, [{element, <<"vCard">>},
  891:                                       {element, <<"PHOTO">>},
  892:                                       {element, <<"BINVAL">>},
  893:                                       cdata]),
  894: 
  895:         ?PHOTO_BASE_64 = Photo,
  896: 
  897:         DirJID = ?config(directory_jid, Config),
  898:         Fields = [{<<"mail">>, <<"alice@mail.example.com">>}],
  899:         Res2 = escalus:send_and_wait(Alice,
  900:                                     escalus_stanza:search_iq(DirJID,
  901:                                                              escalus_stanza:search_fields(Fields))),
  902:         [{JID, ItemTups}] = search_result_item_tuples(Res2),
  903: 
  904:         {_, <<"PHOTO">>, _, ?PHOTO_BASE_64} = lists:keyfind(<<"PHOTO">>, 2, ItemTups)
  905: 
  906:     end).
  907: 
  908: %%------------------------------------
  909: %% @limited.search.domain
  910: 
  911: search_open_limited(Config) ->
  912:     escalus:story(
  913:       Config, [{alice, 1}],
  914:       fun(Client) ->
  915:               Domain = domain(),
  916:               DirJID = <<"directory.", Domain/binary>>,
  917:               Fields = [null],
  918:               Res = escalus:send_and_wait(Client,
  919:                            escalus_stanza:search_iq(DirJID,
  920:                                escalus_stanza:search_fields(Fields))),
  921:               escalus:assert(is_iq_result, Res),
  922:               [] = search_result_item_tuples(Res)
  923:       end).
  924: 
  925: search_some_limited(Config) ->
  926:     escalus:story(
  927:       Config, [{alice, 1}],
  928:       fun(Client) ->
  929:               Domain = ct:get_config({hosts, mim, secondary_domain}),
  930:               DirJID = <<"directory.", Domain/binary>>,
  931:               Fields = [{get_last_name_search_field(), <<"Doe">>}],
  932:               Res = escalus:send_and_wait(Client,
  933:                         escalus_stanza:search_iq(DirJID,
  934:                             escalus_stanza:search_fields(Fields))),
  935:               escalus:assert(is_iq_result, Res),
  936:               ItemTups = search_result_item_tuples(Res),
  937:               %% exactly one result returned and its JID domain is correct
  938:               [{SomeJID, _JIDsFields}] = ItemTups,
  939:               true = lists:member(SomeJID, [<<"aliceb@", Domain/binary>>,
  940:                                             <<"bobb@", Domain/binary>>])
  941:       end).
  942: 
  943: 
  944: %% disco#items to limited.search.domain says directory.limited.search.domain exists
  945: %% disco#info to directory.limited.search.domain says it has feature jabber:iq:search
  946: %% and an <identity category='directory' type='user'/>
  947: %%   http://xmpp.org/extensions/xep-0030.html#registrar-reg-identity
  948: search_in_service_discovery(Config) ->
  949:     escalus:story(
  950:       Config, [{alice, 1}],
  951:       fun(Client) ->
  952:               Domain = domain(),
  953:               DirJID = <<"directory.", Domain/binary>>,
  954: 
  955:               %% Item
  956:               ItemsRes = escalus:send_and_wait(Client,
  957:                                 escalus_stanza:disco_items(Domain)),
  958:               escalus:assert(is_iq_result, ItemsRes),
  959:               escalus:assert(has_item, [DirJID], ItemsRes),
  960: 
  961:               %% Feature
  962:               InfoRes = escalus:send_and_wait(Client,
  963:                             escalus_stanza:disco_info(DirJID)),
  964:               escalus:assert(is_iq_result, InfoRes),
  965:               escalus:assert(has_feature, [?NS_SEARCH], InfoRes),
  966: 
  967:               %% Identity
  968:               escalus:assert(has_identity, [<<"directory">>,
  969:                                             <<"user">>], InfoRes)
  970:       end).
  971: 
  972: %%------------------------------------
  973: %% @no.search.domain
  974: 
  975: search_not_allowed(Config) ->
  976:     escalus:story(
  977:       Config, [{alice, 1}],
  978:       fun(Client) ->
  979:               DirJID = ?config(directory_jid, Config),
  980:               Fields = [null],
  981:               Res = escalus:send_and_wait(Client,
  982:                            escalus_stanza:search_iq(DirJID,
  983:                                escalus_stanza:search_fields(Fields))),
  984:               escalus:assert(fun(Packet) ->
  985:                                      escalus_pred:is_error(<<"cancel">>, <<"service-unavailable">>, Packet)
  986:                                      orelse
  987:                                      %% A case for dynamic domains
  988:                                      escalus_pred:is_error(<<"cancel">>, <<"remote-server-not-found">>, Packet)
  989:                              end, [], Res)
  990:       end).
  991: 
  992: %% disco#items to no.search.domain doesn't say vjud.no.search.domain exists
  993: search_not_in_service_discovery(Config) ->
  994:     escalus:story(
  995:       Config, [{alice, 1}],
  996:       fun(Client) ->
  997:               DirJID = ?config(directory_jid, Config),
  998:               %% Item
  999:               ItemsRes = escalus:send_and_wait(Client,
 1000:                             escalus_stanza:disco_items(domain())),
 1001:               escalus:assert(is_iq_result, ItemsRes),
 1002:               escalus:assert(has_no_such_item, [DirJID], ItemsRes)
 1003:       end).
 1004: 
 1005: %%--------------------------------------------------------------------
 1006: %% Helper functions
 1007: %%--------------------------------------------------------------------
 1008: 
 1009: expected_search_results(Key, Config) ->
 1010:     {_, ExpectedResults} =
 1011:         lists:keyfind(expected_results, 1,
 1012:                       escalus_config:get_config(search_data, Config)),
 1013:     lists:keyfind(Key, 1, ExpectedResults).
 1014: 
 1015: prepare_vcards(Config) ->
 1016:     AllVCards = ?config(all_vcards, Config),
 1017:     ModVcardBackend = get_backend(Config),
 1018:     lists:foreach(
 1019:         fun({JID, Fields}) ->
 1020:                 case binary:match(JID, <<"@">>) of
 1021:                     nomatch ->
 1022:                         ok;
 1023:                     _ ->
 1024:                         prepare_vcard(ModVcardBackend, JID, Fields)
 1025:                 end
 1026:         end, AllVCards),
 1027:     Config.
 1028: 
 1029: get_backend(Config) ->
 1030:     maps:get(backend, ?config(mod_vcard_opts, Config), mnesia).
 1031: 
 1032: prepare_vcard(ldap, JID, Fields) ->
 1033:     [User, Server] = binary:split(JID, <<"@">>),
 1034:     Base = get_ldap_base(Server),
 1035:     VCardMap = [{<<"NICKNAME">>, <<"%u">>, []},
 1036:                 {<<"FN">>, <<"%s">>, [<<"displayName">>]},
 1037:                 {<<"FAMILY">>, <<"%s">>, [<<"sn">>]},
 1038:                 {<<"GIVEN">>, <<"%s">>, [<<"givenName">>]},
 1039:                 {<<"MIDDLE">>, <<"%s">>, [<<"initials">>]},
 1040:                 {<<"ORGNAME">>, <<"%s">>, [<<"o">>]},
 1041:                 {<<"ORGUNIT">>, <<"%s">>, [<<"ou">>]},
 1042: 
 1043:                 %{<<"CTRY">>, <<"%s">>, [<<"c">>]},
 1044:                 {<<"LOCALITY">>, <<"%s">>, [<<"l">>]},
 1045:                 {<<"STREET">>, <<"%s">>, [<<"street">>]},
 1046:                 {<<"REGION">>, <<"%s">>, [<<"st">>]},
 1047: 
 1048:                 {<<"PCODE">>, <<"%s">>, [<<"postalCode">>]},
 1049:                 {<<"TITLE">>, <<"%s">>, [<<"title">>]},
 1050:                 {<<"URL">>, <<"%s">>, [<<"labeleduri">>]},
 1051:                 {<<"DESC">>, <<"%s">>, [<<"description">>]},
 1052:                 {<<"TEL">>, <<"%s">>, [<<"telephoneNumber">>]},
 1053:                 {<<"NUMBER">>, <<"%s">>, [<<"telephoneNumber">>]},
 1054: 
 1055:                 {<<"EMAIL">>, <<"%s">>, [<<"mail">>]},
 1056:                 {<<"USERID">>, <<"%s">>, [<<"mail">>]},
 1057: %%                 {<<"BDAY">>, <<"%s">>, [<<"birthDay">>]}, %OpenLDAP doesn't sport it by default
 1058:                 {<<"ROLE">>, <<"%s">>, [<<"employeeType">>]},
 1059:                 {<<"PHOTO">>, <<"%s">>, [<<"jpegPhoto">>]}
 1060:     ],
 1061:     Fun = fun(Field, Val) ->
 1062:         case vcard_field_to_ldap(VCardMap, Field) of
 1063:             undefined ->
 1064:                 undefined;
 1065:             LdapField ->
 1066:                 eldap:mod_replace(binary_to_list(LdapField), [binary_to_list(Val)])
 1067:         end
 1068:     end,
 1069:     Modificators = convert_vcard_fields(Fields, [], Fun),
 1070:     Dn = <<"cn=", User/binary, ",", Base/binary>>,
 1071:     ok = call_ldap(Server, modify, [binary_to_list(Dn), Modificators]);
 1072: prepare_vcard(_, JID, Fields) ->
 1073:     RJID = get_jid_record(JID),
 1074:     VCard = escalus_stanza:vcard_update(JID, Fields),
 1075:     vcard_rpc(RJID, VCard).
 1076: 
 1077: insert_alice_photo(Config) ->
 1078:     User = <<"alice">>,
 1079:     Domain = domain(),
 1080:     Base = get_ldap_base(Domain),
 1081:     Photo = ?PHOTO_BIN,
 1082:     Modificators = [eldap:mod_replace("jpegPhoto", [binary_to_list(Photo)])],
 1083:     Dn = <<"cn=", User/binary, ",", Base/binary>>,
 1084:     ok = call_ldap(Domain, modify, [binary_to_list(Dn), Modificators]),
 1085:     Config.
 1086: 
 1087: 
 1088: fields_to_ldap_modificators(_, [], Acc) ->
 1089:     Acc;
 1090: fields_to_ldap_modificators(VcardMap, [{Field, Val} | Rest], Acc) when is_binary(Val) ->
 1091:     case vcard_field_to_ldap(VcardMap, Field) of
 1092:         undefined ->
 1093:             NewAcc = Acc;
 1094:         LdapField ->
 1095:             LdapModify = eldap:mod_replace(binary_to_list(LdapField), [binary_to_list(Val)]),
 1096:             NewAcc = [LdapModify | Acc]
 1097:     end,
 1098:     fields_to_ldap_modificators(VcardMap, Rest, NewAcc);
 1099: fields_to_ldap_modificators(VcardMap, [{_, Children} | Rest], Acc) when is_list(Children) ->
 1100:     NewAcc = fields_to_ldap_modificators(VcardMap, Children, Acc),
 1101:     fields_to_ldap_modificators(VcardMap, Rest, NewAcc).
 1102: 
 1103: vcard_field_to_ldap(Map, Field) ->
 1104:     case lists:keyfind(Field, 1, Map) of
 1105:         {Field, _, [LdapField]} ->
 1106:             LdapField;
 1107:         _ ->
 1108:             undefined
 1109:     end.
 1110: 
 1111: delete_vcards(Config) ->
 1112:     AllVCards = escalus_config:get_ct({vcard, data, all_search, expected_vcards}),
 1113:     lists:foreach(
 1114:       fun({JID, _}) ->
 1115:               case binary:match(JID, <<"@">>) of
 1116:                   nomatch ->
 1117:                       ok;
 1118:                   _ ->
 1119:                       RJID = get_jid_record(JID),
 1120:                       ok = vcard_rpc(RJID,
 1121:                                      escalus_stanza:vcard_update(JID, []))
 1122:               end
 1123:       end, AllVCards),
 1124:     Config.
 1125: 
 1126: get_jid_record(JID) ->
 1127:     [User, Server] = binary:split(JID, <<"@">>),
 1128:     jid:make_bare(User, Server).
 1129: 
 1130: vcard_rpc(JID, Stanza) ->
 1131:     Res = rpc(mim(), ejabberd_router, route, [JID, JID, Stanza]),
 1132:     case Res of
 1133:         #{stanza := #{type := <<"set">>}} ->
 1134:             ok;
 1135:         _ ->
 1136:             %% Something is wrong with the IQ handler
 1137:             %% Let's print enough info
 1138:             ct:pal("jid=~p~n stanza=~p~n result=~p~n", [JID, Stanza, Res]),
 1139:             mongoose_helper:print_debug_info_for_module(mod_vcard),
 1140:             ct:fail(vcard_rpc_failed)
 1141:     end.
 1142: 
 1143: restart_vcard_mod(Config, ro_limited) ->
 1144:     restart_mod(params_limited(Config));
 1145: restart_vcard_mod(Config, params_limited_infinity) ->
 1146:     restart_mod(params_limited_infinity(Config));
 1147: restart_vcard_mod(Config, ro_no) ->
 1148:     restart_mod(params_no(Config));
 1149: restart_vcard_mod(Config, ldap_only) ->
 1150:     restart_mod(params_ldap_only(Config));
 1151: restart_vcard_mod(Config, _GN) ->
 1152:     restart_mod(params_all(Config)).
 1153: 
 1154: restart_mod(Params) ->
 1155:     [restart_mod(HostType, Params) || HostType <- host_types()],
 1156:     ok.
 1157: 
 1158: restart_mod(HostType, Params) ->
 1159:     dynamic_modules:restart(HostType, mod_vcard, Params).
 1160: 
 1161: prepare_vcard_module(Config) ->
 1162:     %% Keep the old config, so we can undo our changes, once finished testing
 1163:     Config1 = dynamic_modules:save_modules(host_types(), Config),
 1164:     %% Get a list of options, we can use as a prototype to start new modules
 1165:     VCardOpts = dynamic_modules:get_saved_config(host_type(), mod_vcard, Config1),
 1166:     [{mod_vcard_opts, VCardOpts} | Config1].
 1167: 
 1168: restore_vcard_module(Config) ->
 1169:     dynamic_modules:restore_modules(Config).
 1170: 
 1171: stop_vcard_mod(_Config) ->
 1172:     [dynamic_modules:stop(HostType, mod_vcard) || HostType <- host_types()],
 1173:     ok.
 1174: 
 1175: params_all(Config) ->
 1176:     add_backend_param(#{}, ?config(mod_vcard_opts, Config)).
 1177: 
 1178: params_limited(Config) ->
 1179:     add_backend_param(#{matches => 1,
 1180:                         host => subhost_pattern("directory.@HOST@")},
 1181:                       ?config(mod_vcard_opts, Config)).
 1182: 
 1183: params_limited_infinity(Config) ->
 1184:     add_backend_param(#{matches => infinity,
 1185:                         host => subhost_pattern("directory.@HOST@")},
 1186:                       ?config(mod_vcard_opts, Config)).
 1187: 
 1188: params_no(Config) ->
 1189:     add_backend_param(#{search => false,
 1190:                         host => subhost_pattern("vjud.@HOST@")},
 1191:                       ?config(mod_vcard_opts, Config)).
 1192: 
 1193: params_ldap_only(Config) ->
 1194:     Reported = [{<<"Full Name">>, <<"FN">>},
 1195:                 {<<"Given Name">>, <<"FIRST">>},
 1196:                 {<<"Middle Name">>, <<"MIDDLE">>},
 1197:                 {<<"Family Name">>, <<"LAST">>},
 1198:                 {<<"Nickname">>, <<"NICK">>},
 1199:                 {<<"Birthday">>, <<"BDAY">>},
 1200:                 {<<"Country">>, <<"CTRY">>},
 1201:                 {<<"City">>, <<"LOCALITY">>},
 1202:                 {<<"Email">>, <<"EMAIL">>},
 1203:                 {<<"Organization Name">>, <<"ORGNAME">>},
 1204:                 {<<"Organization Unit">>, <<"ORGUNIT">>},
 1205:                 {<<"Photo">>, <<"PHOTO">>}],
 1206:     add_backend_param(#{ldap => #{search_operator => 'or',
 1207:                                   binary_search_fields => [<<"PHOTO">>],
 1208:                                   search_reported => Reported}},
 1209:                       ?config(mod_vcard_opts, Config)).
 1210: 
 1211: add_backend_param(Opts = #{ldap := LDAPOpts},
 1212:                   CurrentVCardConfig = #{backend := ldap, ldap := CurrentLDAPOpts}) ->
 1213:     NewLDAPOpts = maps:merge(CurrentLDAPOpts, LDAPOpts),
 1214:     maps:merge(CurrentVCardConfig, Opts#{ldap => NewLDAPOpts});
 1215: add_backend_param(Opts, CurrentVCardConfig) ->
 1216:     maps:merge(CurrentVCardConfig, Opts).
 1217: 
 1218: %%----------------------
 1219: %% xmlel shortcuts
 1220: stanza_get_vcard_field(Stanza, FieldName) ->
 1221:     VCard = ?EL(Stanza, <<"vCard">>),
 1222:     ?EL(VCard, FieldName).
 1223: 
 1224: stanza_get_vcard_field_cdata(Stanza, FieldName) ->
 1225:     VCard = ?EL(Stanza, <<"vCard">>),
 1226:     ?EL_CD(VCard, FieldName).
 1227: 
 1228: %%---------------------
 1229: %% test helpers
 1230: 
 1231: %%
 1232: %% -> [{Type, Var, Label}]
 1233: %%
 1234: field_tuples([]) ->
 1235:     [];
 1236: field_tuples([#xmlel{name = <<"field">>,
 1237:                           attrs=Attrs,
 1238:                           children=_Children} = El| Rest]) ->
 1239:     {<<"type">>, Type} = lists:keyfind(<<"type">>, 1, Attrs),
 1240:     {<<"var">>, Var} = lists:keyfind(<<"var">>, 1, Attrs),
 1241:     {<<"label">>, Label} = lists:keyfind(<<"label">>, 1, Attrs),
 1242:     case ?EL_CD(El, <<"value">>) of
 1243:         undefined ->
 1244:             [{Type, Var, Label}|field_tuples(Rest)];
 1245:         ValCData ->
 1246:             [{Type, Var, Label, ValCData}|field_tuples(Rest)]
 1247:     end;
 1248: field_tuples([_SomeOtherEl|Rest]) ->
 1249:     field_tuples(Rest).
 1250: 
 1251: 
 1252: %%
 1253: %%  -> [{Type, Var, Label, ValueCData}]
 1254: %%
 1255: %% This is naiive and expensive LOL!
 1256: item_field_tuples(_, []) ->
 1257:     [];
 1258: item_field_tuples(ReportedFieldTups,
 1259:                   [#xmlel{name = <<"field">>,
 1260:                                attrs=Attrs,
 1261:                                children=_Children} = El| Rest]) ->
 1262:     {<<"var">>, Var} = lists:keyfind(<<"var">>, 1, Attrs),
 1263:     {Type, Var, Label} = lists:keyfind(Var, 2, ReportedFieldTups),
 1264:     [{Type, Var, Label, ?EL_CD(El, <<"value">>)}
 1265:      | item_field_tuples(ReportedFieldTups, Rest)];
 1266: 
 1267: item_field_tuples(ReportedFieldTups, [_SomeOtherEl|Rest]) ->
 1268:     item_field_tuples(ReportedFieldTups, Rest).
 1269: 
 1270: 
 1271: %%
 1272: %% -> [{JID, [ItemFieldTups]}]
 1273: %%
 1274: %% Finds the JID and maps fields to their labels and types
 1275: %%
 1276: item_tuples(_, []) ->
 1277:     [];
 1278: item_tuples(ReportedFieldTups, [#xmlel{name = <<"item">>,
 1279:                                             children = Children} | Rest]) ->
 1280:     ItemFieldTups = item_field_tuples(ReportedFieldTups, Children),
 1281:     {_, _, _, JID} = lists:keyfind(<<"jid">>, 2, ItemFieldTups),
 1282:     [{JID, ItemFieldTups}|item_tuples(ReportedFieldTups, Rest)];
 1283: item_tuples(ReportedFieldTypes, [_SomeOtherChild | Rest]) ->
 1284:     item_tuples(ReportedFieldTypes, Rest).
 1285: 
 1286: 
 1287: %% This tests that at least the values in the ExpectedVCardTups are in the
 1288: %% VCardUnderTest.
 1289: %% Any extra values in the vcard are ignored by this function and should be
 1290: %% checked or rejected elsewhere.
 1291: %% crash means fail, return means success.
 1292: check_vcard(ExpectedVCardTups, Stanza) ->
 1293:     escalus_pred:is_iq(<<"result">>, Stanza),
 1294:     VCardUnderTest = ?EL(Stanza, <<"vCard">>),
 1295:     check_xml_element(ExpectedVCardTups, VCardUnderTest).
 1296: 
 1297: 
 1298: check_xml_element([], _ElUnderTest) ->
 1299:     ok;  %% just return true to be consistent with other clauses.
 1300: check_xml_element([{ExpdFieldName, ExpdChildren}|Rest], ElUnderTest)
 1301:   when is_list(ExpdChildren) ->
 1302:     check_xml_element(ExpdChildren, ?EL(ElUnderTest, ExpdFieldName)),
 1303:     check_xml_element(Rest, ElUnderTest);
 1304: check_xml_element([{ExpdFieldName, ExpdCData}|Rest], ElUnderTest) ->
 1305:     case ?EL_CD(ElUnderTest, ExpdFieldName) of
 1306:         ExpdCData ->
 1307:             check_xml_element(Rest, ElUnderTest);
 1308:         Else ->
 1309:             ct:fail("Expected ~p got ~p~n", [ExpdCData, Else])
 1310:     end.
 1311: 
 1312: %% Checks that the elements of two lists with matching keys are equal
 1313: %% while the order of the elements does not matter.
 1314: %% Returning means success. Crashing via ct:fail means failure.
 1315: %% Prints the lists in the ct:fail Result term.
 1316: list_unordered_key_match(Expected, Actual) ->
 1317:     case length(Actual) of
 1318:         ActualLength when ActualLength == length(Expected) ->
 1319:             list_unordered_key_match2(Expected, Actual);
 1320:         ActualLength ->
 1321:             ct:fail("Expected size ~p, actual size ~p~nExpected: ~p~nActual: ~p",
 1322:                     [length(Expected), ActualLength, Expected, Actual])
 1323:     end.
 1324: 
 1325: list_unordered_key_match2([], _) ->
 1326:     ok;
 1327: list_unordered_key_match2([{User, ExpectedTup} | _Rest], ActualTuples) ->
 1328: 
 1329:     case lists:keyfind(User, 1, ActualTuples) of
 1330:         {User, ReceivedTuple} ->
 1331:             verify_tuples(ReceivedTuple, ExpectedTup);
 1332:         _ ->
 1333:             ct:fail("can't find user ~p in received results: ~p",
 1334:                     [User, ActualTuples])
 1335: 
 1336:     end.
 1337: 
 1338: verify_tuples(Received, Expected) ->
 1339:     Fun = fun({_, _, Name, Value} = ExpectedItem) ->
 1340:         case lists:keyfind(Name, 3, Received) of
 1341:             {_, _, Name, Value} ->
 1342:                 true;
 1343:             _ ->
 1344:                 ct:fail("can't find item ~p in received items:~p", [ExpectedItem, Received])
 1345:         end
 1346:     end,
 1347:     lists:all(Fun, Expected).
 1348: 
 1349: 
 1350: search_result_item_tuples(Stanza) ->
 1351:     Result = ?EL(Stanza, <<"query">>),
 1352:     XData = ?EL(Result, <<"x">>),
 1353:     #xmlel{ attrs = _XAttrs,
 1354:                  children = XChildren } = XData,
 1355:     Reported = ?EL(XData, <<"reported">>),
 1356:     ReportedFieldTups = field_tuples(Reported#xmlel.children),
 1357:     _ItemTups = item_tuples(ReportedFieldTups, XChildren).
 1358: 
 1359: get_all_vcards() ->
 1360:     Domain = domain(),
 1361:     SecDomain = secondary_domain(),
 1362:     [{<<"alice@", Domain/binary>>,
 1363:       [{<<"NICKNAME">>, <<"alice">>},
 1364:        {<<"FN">>, <<"Wonderland, Alice">>},
 1365:        {<<"TITLE">>, <<"Executive Director">>},
 1366:        {<<"ROLE">>, <<"Patron Saint">>},
 1367:        {<<"DESC">>, <<"active">>},
 1368:        {<<"URL">>, <<"http://john.doe/">>},
 1369:        %{<<"PHOTO">>, crypto:strong_rand_bytes(10)},
 1370:        {<<"EMAIL">>,
 1371:         [{<<"USERID">>, <<"alice@mail.example.com">>}]},
 1372:        {<<"N">>,
 1373:         [{<<"FAMILY">>, <<"Wonderland">>},
 1374:          {<<"GIVEN">>, <<"Alice">>},
 1375:          {<<"MIDDLE">>, <<"I.N.">>}]},
 1376:        {<<"ADR">>,
 1377:         [{<<"STREET">>, <<"1899 Wynkoop Street">>},
 1378:          {<<"LOCALITY">>, get_utf8_city()},
 1379:          {<<"REGION">>, <<"CO">>},
 1380:          {<<"PCODE">>, <<"91210">>}
 1381:         ] ++ maybe_add_ctry()},
 1382:        {<<"TEL">>,
 1383:         [{<<"NUMBER">>, <<"+1 512 305 0280">>}]},
 1384:        {<<"ORG">>,
 1385:         [{<<"ORGNAME">>, <<"The world">>},
 1386:          {<<"ORGUNIT">>, <<"People">>}]}
 1387:       ] ++ maybe_add_bday() ++ maybe_add_jabberd_id(<<"alice@", Domain/binary>>)},
 1388:      {<<"bob@", Domain/binary>>,
 1389:       [{<<"NICKNAME">>, <<"bob">>},
 1390:        {<<"FN">>, <<"Doe, Bob">>},
 1391:        {<<"ADR">>, [{<<"STREET">>,
 1392:            <<208, 146, 32, 208, 161, 208, 190, 208, 178,
 1393:            208, 181, 209, 130, 209, 129, 208, 186, 208, 190, 208, 185, 32,
 1394:            208, 160, 208, 190, 209, 129, 209, 129, 208, 184, 208, 184, 44,
 1395:            32, 208, 180, 208, 190, 209, 128, 208, 190, 208, 179, 208, 176,
 1396:            32, 209, 128, 208, 176, 208, 183, 208, 178, 208, 181, 209, 130,
 1397:            208, 178, 208, 187, 209, 143, 208, 181, 209, 130, 209, 129, 209,
 1398:            143, 32, 208, 178, 209, 139>>}]}
 1399:       ] ++ maybe_add_jabberd_id(<<"bob@", Domain/binary>>)},
 1400:      {<<"bobb@", SecDomain/binary>>,
 1401:       [{<<"NICKNAME">>, <<"bobb">>},
 1402:        {<<"FN">>, <<"Doe, Bob">>},
 1403:        {<<"N">>,
 1404:         [{<<"FAMILY">>, <<"Doe">>},
 1405:          {<<"GIVEN">>, <<"Bob">>}]}
 1406:       ] ++ maybe_add_jabberd_id(<<"bobb@", SecDomain/binary>>)},
 1407:      {<<"aliceb@", SecDomain/binary>>,
 1408:       [{<<"NICKNAME">>, <<"aliceb">>},
 1409:        {<<"FN">>, <<"Doe, Alice">>},
 1410:        {<<"N">>,
 1411:         [{<<"FAMILY">>, <<"Doe">>},
 1412:          {<<"GIVEN">>, <<"Alice">>}]}
 1413:       ] ++ maybe_add_jabberd_id(<<"aliceb@", SecDomain/binary>>)}
 1414:     ].
 1415: 
 1416: maybe_add_ctry() ->
 1417:     maybe_add([{<<"CTRY">>, <<"PL">>}]).
 1418: 
 1419: maybe_add_bday() ->
 1420:     maybe_add([{<<"BDAY">>, <<"2011-08-06">>}]).
 1421: 
 1422: maybe_add_jabberd_id(JabberId) ->
 1423:     maybe_add([{<<"JABBERID">>, JabberId}]).
 1424: 
 1425: maybe_add(Elems) ->
 1426:     case vcard_helper:is_vcard_ldap() of
 1427:         true ->
 1428:             [];
 1429:         _ ->
 1430:             Elems
 1431:     end.
 1432: 
 1433: get_server_vcards() ->
 1434:     [{domain(),
 1435:       [{<<"FN">>, <<"MongooseIM">>},
 1436:        {<<"DESC">>, <<"MongooseIM XMPP Server\nCopyright (c) Erlang Solutions Ltd.">>}]},
 1437:      {<<"vjud.", (domain())/binary>>,
 1438:       [{<<"FN">>, <<"MongooseIM/mod_vcard">>},
 1439:        {<<"DESC">>, <<"MongooseIM vCard module\nCopyright (c) Erlang Solutions Ltd.">>}]}].
 1440: 
 1441: 
 1442: get_user_vcard(JID, Config) ->
 1443:     case lists:keyfind(JID, 1, ?config(all_vcards, Config)) of
 1444:         {JID, ClientVCardTups} ->
 1445:             ClientVCardTups;
 1446:         _ ->
 1447:             ct:fail({get_user_vcard_failed, JID})
 1448:     end.
 1449: 
 1450: get_server_vcard(ServerJid, Config) ->
 1451:     case lists:keyfind(ServerJid, 1, ?config(server_vcards, Config)) of
 1452:         {ServerJid, VCard} ->
 1453:             VCard;
 1454:         _ ->
 1455:             ct:fail({get_server_vcard_failed, ServerJid})
 1456:     end.
 1457: 
 1458: get_search_results(Config, Users) ->
 1459:     [{User, get_search_result(get_user_vcard(User, Config))} || User <- Users].
 1460: 
 1461: get_search_result(VCard) ->
 1462:     convert_vcard_fields(VCard, [], fun vcard_field_to_result/2).
 1463: 
 1464: get_locality_search_field() ->
 1465:     get_search_field(<<"locality">>, <<"l">>).
 1466: 
 1467: get_user_search_field() ->
 1468:     get_search_field(<<"user">>, <<"%u">>).
 1469: 
 1470: get_full_name_search_field() ->
 1471:     get_search_field(<<"fn">>, <<"displayName">>).
 1472: 
 1473: get_last_name_search_field() ->
 1474:     get_search_field(<<"last">>, <<"sn">>).
 1475: 
 1476: get_first_name_search_field() ->
 1477:     get_search_field(<<"first">>, <<"givenName">>).
 1478: 
 1479: get_search_field(Default, LDAP) ->
 1480:     case vcard_helper:is_vcard_ldap() of
 1481:         true ->
 1482:             LDAP;
 1483:         _ ->
 1484:             Default
 1485:     end.
 1486: 
 1487: convert_vcard_fields([], Acc, _) -> Acc;
 1488: convert_vcard_fields([{_Field, Children} | Rest], Acc, Fun) when is_list(Children) ->
 1489:     NewAcc = convert_vcard_fields(Children, Acc, Fun),
 1490:     convert_vcard_fields(Rest, NewAcc, Fun);
 1491: convert_vcard_fields([{Field, Value} | Rest], Acc, Fun) ->
 1492:     NewAcc = case Fun(Field, Value) of
 1493:                  undefined ->
 1494:                      Acc;
 1495:                  Result ->
 1496:                      [Result | Acc]
 1497:     end,
 1498:     convert_vcard_fields(Rest, NewAcc, Fun).
 1499: 
 1500: 
 1501: vcard_field_to_result(Field, Value) ->
 1502:     case vcard_result_mapping(Field) of
 1503:         undefined ->
 1504:             undefined;
 1505:         Short ->
 1506:             case lists:keyfind(Short, 2, reported_fields()) of
 1507:                 {Type, Short, Long} ->
 1508:                     {Type, Short, Long, Value};
 1509:                 _ ->
 1510:                     undefined
 1511:             end
 1512:     end.
 1513: 
 1514: reported_fields() ->
 1515:     [{<<"text-single">>, <<"fn">>, <<"Full Name">>},
 1516:      {<<"text-single">>, <<"last">>, <<"Family Name">>},
 1517:      {<<"text-single">>, <<"first">>, <<"Name">>},
 1518:      {<<"text-single">>, <<"middle">>, <<"Middle Name">>},
 1519:      {<<"text-single">>, <<"nick">>, <<"Nickname">>},
 1520:      {<<"text-single">>, <<"bday">>, <<"Birthday">>},
 1521:      {<<"text-single">>, <<"ctry">>, <<"Country">>},
 1522:      {<<"text-single">>, <<"locality">>, <<"City">>},
 1523:      {<<"text-single">>, <<"email">>, <<"Email">>},
 1524:      {<<"text-single">>, <<"orgname">>, <<"Organization Name">>},
 1525:      {<<"text-single">>, <<"orgunit">>, <<"Organization Unit">>}
 1526:     ] ++ maybe_add_jabberd_field().
 1527: 
 1528: maybe_add_jabberd_field() ->
 1529:     case vcard_helper:is_vcard_ldap() of
 1530:         true ->
 1531:             [];
 1532:         _ ->
 1533:             [{<<"jid-single">>, <<"jid">>, <<"Jabber ID">>}]
 1534:     end.
 1535: 
 1536: 
 1537: vcard_result_mapping(<<"NICKNAME">>) -> <<"nick">>;
 1538: vcard_result_mapping(<<"FN">>) -> <<"fn">>;
 1539: vcard_result_mapping(<<"USERID">>) -> <<"email">>;
 1540: vcard_result_mapping(<<"FAMILY">>) -> <<"last">>;
 1541: vcard_result_mapping(<<"GIVEN">>) -> <<"first">>;
 1542: vcard_result_mapping(<<"MIDDLE">>) -> <<"middle">>;
 1543: vcard_result_mapping(<<"LOCALITY">>) -> <<"locality">>;
 1544: vcard_result_mapping(<<"CTRY">>) -> <<"ctry">>;
 1545: vcard_result_mapping(<<"ORGNAME">>) -> <<"orgname">>;
 1546: vcard_result_mapping(<<"ORGUNIT">>) -> <<"orgunit">>;
 1547: vcard_result_mapping(<<"JABBERID">>) -> <<"jid">>;
 1548: vcard_result_mapping(<<"BDAY">>) -> <<"bday">>;
 1549: vcard_result_mapping(_) -> undefined.
 1550: 
 1551: get_utf8_city() ->
 1552:     %% This is the UTF-8 of Москва
 1553:     <<208, 156, 208, 190, 209, 129, 208, 186, 208, 178, 208, 176>>.
 1554: 
 1555: secondary_domain() ->
 1556:     ct:get_config({hosts, mim, secondary_domain}).