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