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