github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/service/usersearch_test.go (about)

     1  // Copyright 2019 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package service
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"sort"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/keybase/client/go/contacts"
    14  	"github.com/keybase/client/go/protocol/keybase1"
    15  	"golang.org/x/net/context"
    16  
    17  	"github.com/keybase/client/go/kbtest"
    18  	"github.com/keybase/client/go/libkb"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  type emptyUserSearchProvider struct{}
    23  
    24  func (*emptyUserSearchProvider) MakeSearchRequest(mctx libkb.MetaContext, arg keybase1.UserSearchArg) ([]keybase1.APIUserSearchResult, error) {
    25  	return nil, nil
    26  }
    27  
    28  type searchResultForTest struct {
    29  	id              string
    30  	displayName     string
    31  	displayLabel    string
    32  	keybaseUsername string
    33  }
    34  
    35  // pluckSearchResultForTest normalizes APIUserSearchResult data for making test
    36  // assertion code simpler.
    37  func pluckSearchResultForTest(apiRes keybase1.APIUserSearchResult) searchResultForTest {
    38  	// TODO: For Y2K-310, usersearch handler will just return data like this,
    39  	// so this kind of normalization will not be needed.
    40  	switch {
    41  	case apiRes.Service != nil:
    42  		// Logic from shared/constants/team-building.tsx
    43  		var keybaseUsername string
    44  		var id string
    45  		label := apiRes.Service.FullName
    46  		if apiRes.Keybase != nil {
    47  			keybaseUsername = apiRes.Keybase.Username
    48  			id = keybaseUsername
    49  			if label == "" {
    50  				if apiRes.Keybase.FullName != nil {
    51  					label = *apiRes.Keybase.FullName
    52  				} else {
    53  					label = keybaseUsername
    54  				}
    55  			}
    56  		} else {
    57  			id = fmt.Sprintf("%s@%s", apiRes.Service.Username, apiRes.Service.ServiceName)
    58  		}
    59  		return searchResultForTest{
    60  			id:              id,
    61  			displayName:     apiRes.Service.Username,
    62  			displayLabel:    label,
    63  			keybaseUsername: keybaseUsername,
    64  		}
    65  	case apiRes.Contact != nil:
    66  		return searchResultForTest{
    67  			id:              apiRes.Contact.Assertion,
    68  			displayName:     apiRes.Contact.DisplayName,
    69  			displayLabel:    apiRes.Contact.DisplayLabel,
    70  			keybaseUsername: apiRes.Contact.Username,
    71  		}
    72  	case apiRes.Imptofu != nil:
    73  		return searchResultForTest{
    74  			id:              apiRes.Imptofu.Assertion,
    75  			displayName:     apiRes.Imptofu.PrettyName,
    76  			displayLabel:    apiRes.Imptofu.Label,
    77  			keybaseUsername: apiRes.Imptofu.KeybaseUsername,
    78  		}
    79  	case apiRes.Keybase != nil:
    80  		var fullName string
    81  		if apiRes.Keybase.FullName != nil {
    82  			fullName = *apiRes.Keybase.FullName
    83  		}
    84  		return searchResultForTest{
    85  			id:              apiRes.Keybase.Username,
    86  			displayName:     apiRes.Keybase.Username,
    87  			displayLabel:    fullName,
    88  			keybaseUsername: apiRes.Keybase.Username,
    89  		}
    90  	default:
    91  		panic("unexpected APIUserSearchResult for pluckSearchResultForTest")
    92  	}
    93  }
    94  
    95  func pluckAllSearchResultForTest(apiRes []keybase1.APIUserSearchResult) (res []searchResultForTest) {
    96  	res = make([]searchResultForTest, len(apiRes))
    97  	for i, v := range apiRes {
    98  		res[i] = pluckSearchResultForTest(v)
    99  	}
   100  	return res
   101  }
   102  
   103  type makeContactArg struct {
   104  	index int
   105  
   106  	name  string
   107  	label string
   108  	phone string
   109  	email string
   110  
   111  	// if resolved
   112  	username  string
   113  	fullname  string
   114  	following bool
   115  }
   116  
   117  func makeContact(arg makeContactArg) (res keybase1.ProcessedContact) {
   118  	res.ContactIndex = arg.index
   119  	res.ContactName = arg.name
   120  	res.Component.Label = arg.label
   121  	var componentValue string
   122  	if arg.phone != "" {
   123  		componentValue = arg.phone
   124  		phone := keybase1.RawPhoneNumber(arg.phone)
   125  		res.Component.PhoneNumber = &phone
   126  		res.Assertion = fmt.Sprintf("%s@phone", strings.TrimLeft(componentValue, "+"))
   127  	} else if arg.email != "" {
   128  		componentValue = arg.email
   129  		email := keybase1.EmailAddress(arg.email)
   130  		res.Component.Email = &email
   131  		res.Assertion = fmt.Sprintf("[%s]@email", componentValue)
   132  	}
   133  	if arg.username != "" {
   134  		res.Username = arg.username
   135  		res.Uid = libkb.UsernameToUID(arg.username)
   136  		res.Resolved = true
   137  		res.Following = arg.following
   138  	}
   139  	// Emulate contact sync display name/label generation. Will not be needed
   140  	// once Y2K-310 is done where we move all that logic to usersearch.
   141  	if arg.username != "" {
   142  		res.DisplayName = arg.username
   143  		if arg.following && arg.fullname != "" {
   144  			res.DisplayLabel = arg.fullname
   145  		} else if arg.name != "" {
   146  			res.DisplayLabel = arg.name
   147  		} else {
   148  			res.DisplayLabel = componentValue
   149  		}
   150  	} else {
   151  		res.DisplayName = arg.name
   152  		if arg.label != "" {
   153  			res.DisplayLabel = fmt.Sprintf("%s (%s)", componentValue, arg.label)
   154  		} else {
   155  			res.DisplayLabel = componentValue
   156  		}
   157  	}
   158  	return res
   159  }
   160  
   161  func stringifyAPIResult(list []keybase1.APIUserSearchResult) (res []string) {
   162  	res = make([]string, 0, len(list))
   163  	for _, v := range list {
   164  		if v.Contact != nil {
   165  			res = append(res, fmt.Sprintf("%s,%s", v.Contact.DisplayName, v.Contact.DisplayLabel))
   166  		}
   167  	}
   168  	return res
   169  }
   170  
   171  type testKeybaseUserSearchData struct {
   172  	username   string
   173  	fullName   string
   174  	serviceMap map[string]string
   175  	followee   bool
   176  }
   177  
   178  type testUserSearchProvider struct {
   179  	T     *testing.T
   180  	users []testKeybaseUserSearchData
   181  }
   182  
   183  type testAddUserArg struct {
   184  	username string
   185  	fullName string
   186  }
   187  
   188  func (p *testUserSearchProvider) addUser(args ...testAddUserArg) {
   189  	for _, arg := range args {
   190  		user := testKeybaseUserSearchData{
   191  			username: arg.username,
   192  			fullName: arg.fullName,
   193  		}
   194  		p.users = append(p.users, user)
   195  	}
   196  }
   197  
   198  func (p *testUserSearchProvider) MakeSearchRequest(mctx libkb.MetaContext, arg keybase1.UserSearchArg) (res []keybase1.APIUserSearchResult, err error) {
   199  	if arg.Service != "keybase" && arg.Service != "" {
   200  		p.T.Errorf("unexpected service to MakeSearchRequest: %q", arg.Service)
   201  		return nil, errors.New("unexpected service")
   202  	}
   203  
   204  	// Use functions for contacts searching to emulate server behavior here.
   205  	query, err := compileQuery(arg.Query)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	for _, user := range p.users {
   211  		var found bool
   212  		var score float64
   213  		if found, score = query.scoreString(user.username); found {
   214  			// noop, query matched username
   215  		} else if found, score = query.scoreString(user.fullName); found {
   216  			// noop, query matched full name
   217  		} else if user.serviceMap != nil {
   218  			for _, serviceUser := range user.serviceMap {
   219  				if found, score = query.scoreString(serviceUser); found {
   220  					// query matched one of the services, break out of
   221  					// serviceMap loop
   222  					break
   223  				}
   224  			}
   225  		}
   226  		if found {
   227  			var fullname *string
   228  			if user.fullName != "" {
   229  				fn := user.fullName
   230  				fullname = &fn
   231  			}
   232  			keybase := keybase1.APIUserKeybaseResult{
   233  				Username:   user.username,
   234  				Uid:        libkb.UsernameToUID(user.username),
   235  				FullName:   fullname,
   236  				RawScore:   score,
   237  				IsFollowee: user.followee,
   238  			}
   239  			res = append(res, keybase1.APIUserSearchResult{Keybase: &keybase})
   240  		}
   241  	}
   242  
   243  	sort.Slice(res, func(i, j int) bool {
   244  		return res[i].Keybase.RawScore > res[j].Keybase.RawScore
   245  	})
   246  	for i := range res {
   247  		res[i].Score = 1.0 / float64(1+i)
   248  	}
   249  	return res, nil
   250  }
   251  
   252  func setupUserSearchTest(t *testing.T) (tc libkb.TestContext, handler *UserSearchHandler, searchProv *testUserSearchProvider) {
   253  	tc = libkb.SetupTest(t, "contacts", 3)
   254  	tc.G.SyncedContactList = contacts.NewSavedContactsStore(tc.G)
   255  
   256  	_, err := kbtest.CreateAndSignupFakeUser("lmu", tc.G)
   257  	require.NoError(t, err)
   258  
   259  	contactsProv := &contacts.CachedContactsProvider{
   260  		// Usersearch tests will call this provider for imp tofu searches, we
   261  		// want it to return errors but not fail entire test.
   262  		Provider: &contacts.ErrorContactsProvider{T: t, NoFail: true},
   263  		Store:    contacts.NewContactCacheStore(tc.G),
   264  	}
   265  	handler = NewUserSearchHandler(nil, tc.G, contactsProv)
   266  	searchProv = &testUserSearchProvider{T: t}
   267  	handler.searchProvider = searchProv
   268  	return tc, handler, searchProv
   269  }
   270  
   271  func TestContactSearch(t *testing.T) {
   272  	tc := libkb.SetupTest(t, "contacts", 3)
   273  	defer tc.Cleanup()
   274  
   275  	_, err := kbtest.CreateAndSignupFakeUser("lmu", tc.G)
   276  	require.NoError(t, err)
   277  
   278  	contactlist := []keybase1.ProcessedContact{
   279  		makeContact(makeContactArg{index: 0, name: "Test Contact 1", username: "tuser1"}),
   280  		makeContact(makeContactArg{index: 1, name: "Office Building", phone: "+1123"}),
   281  		makeContact(makeContactArg{index: 2, name: "Michal", username: "michal"}),
   282  		makeContact(makeContactArg{index: 3, name: "TEST", phone: "+1555123456"}),
   283  	}
   284  
   285  	contactsProv := NewCachedContactsProvider(tc.G)
   286  	savedStore := contacts.NewSavedContactsStore(tc.G)
   287  	err = savedStore.SaveProcessedContacts(tc.MetaContext(), contactlist)
   288  	require.NoError(t, err)
   289  	tc.G.SyncedContactList = savedStore
   290  
   291  	searchHandler := NewUserSearchHandler(nil, tc.G, contactsProv)
   292  	searchHandler.searchProvider = &emptyUserSearchProvider{}
   293  
   294  	// Invalid: `IncludeContacts` can only be passed with service="keybase"
   295  	// (even if query is empty).
   296  	_, err = searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   297  		IncludeContacts: true,
   298  		Service:         "",
   299  		Query:           "",
   300  	})
   301  	require.Error(t, err)
   302  
   303  	res, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   304  		IncludeContacts: true,
   305  		Service:         "keybase",
   306  		Query:           "test",
   307  		MaxResults:      50,
   308  	})
   309  	require.NoError(t, err)
   310  	require.Len(t, res, 2)
   311  	strList := stringifyAPIResult(res)
   312  	require.Contains(t, strList, "TEST,+1555123456")
   313  	require.Contains(t, strList, "tuser1,Test Contact 1")
   314  
   315  	res, err = searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   316  		IncludeContacts: true,
   317  		Service:         "keybase",
   318  		Query:           "building",
   319  	})
   320  	require.NoError(t, err)
   321  	require.Len(t, res, 1)
   322  	strList = stringifyAPIResult(res)
   323  	require.Contains(t, strList, "Office Building,+1123")
   324  }
   325  
   326  func TestContactSearchWide(t *testing.T) {
   327  	tc := libkb.SetupTest(t, "contacts", 3)
   328  	defer tc.Cleanup()
   329  
   330  	_, err := kbtest.CreateAndSignupFakeUser("lmu", tc.G)
   331  	require.NoError(t, err)
   332  
   333  	contactlist := []keybase1.ProcessedContact{
   334  		makeContact(makeContactArg{name: "Test Contact 1", username: "tuser1"}),
   335  		makeContact(makeContactArg{name: "🍱🍜🍪 Lunch", phone: "+48123"}),
   336  		makeContact(makeContactArg{name: "Michal", username: "michal"}),
   337  		makeContact(makeContactArg{name: "高橋幸治", phone: "+81123456555"}),
   338  	}
   339  
   340  	contactsProv := NewCachedContactsProvider(tc.G)
   341  	savedStore := contacts.NewSavedContactsStore(tc.G)
   342  	err = savedStore.SaveProcessedContacts(tc.MetaContext(), contactlist)
   343  	require.NoError(t, err)
   344  	tc.G.SyncedContactList = savedStore
   345  
   346  	searchHandler := NewUserSearchHandler(nil, tc.G, contactsProv)
   347  	searchHandler.searchProvider = &emptyUserSearchProvider{}
   348  
   349  	res, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   350  		IncludeContacts: true,
   351  		Service:         "keybase",
   352  		Query:           "高橋",
   353  	})
   354  	require.NoError(t, err)
   355  	require.Len(t, res, 1)
   356  	require.Equal(t, "高橋幸治", res[0].Contact.DisplayName)
   357  
   358  	for _, v := range []string{"🍜", "🍱", "lunch"} {
   359  		res, err = searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   360  			IncludeContacts: true,
   361  			Service:         "keybase",
   362  			Query:           v,
   363  		})
   364  		require.NoError(t, err)
   365  		require.Len(t, res, 1)
   366  		require.Equal(t, "🍱🍜🍪 Lunch", res[0].Contact.DisplayName)
   367  	}
   368  }
   369  
   370  func TestUserSearchResolvedUsersShouldGoFirst(t *testing.T) {
   371  	tc, searchHandler, searchProv := setupUserSearchTest(t)
   372  	defer tc.Cleanup()
   373  
   374  	contactlist := []keybase1.ProcessedContact{
   375  		makeContact(makeContactArg{index: 0, name: "TEST", phone: "+1555123456"}),
   376  		makeContact(makeContactArg{index: 1, name: "Michal", email: "michal@example.com", username: "michal"}),
   377  		makeContact(makeContactArg{index: 2, name: "Test Contact 1", phone: "+1555165432", username: "tuser1", fullname: "Test User 123"}),
   378  	}
   379  
   380  	searchProv.addUser(testAddUserArg{"tuser1", "Test User 123"})
   381  
   382  	err := tc.G.SyncedContactList.SaveProcessedContacts(tc.MetaContext(), contactlist)
   383  	require.NoError(t, err)
   384  
   385  	// "1555" query will match two users: name: "TEST", and name: "Test Contact
   386  	// 1". We should see the resolved one appear first, with the matched string
   387  	// (the phone number) being the label.
   388  	res, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   389  		IncludeContacts: true,
   390  		Service:         "keybase",
   391  		Query:           "1555",
   392  		MaxResults:      50,
   393  	})
   394  	require.NoError(t, err)
   395  	require.Len(t, res, 2)
   396  	require.Equal(t, searchResultForTest{
   397  		id:              "1555165432@phone",
   398  		displayName:     "tuser1",
   399  		displayLabel:    "+1555165432",
   400  		keybaseUsername: "tuser1",
   401  	}, pluckSearchResultForTest(res[0]))
   402  
   403  	require.Equal(t, searchResultForTest{
   404  		id:              "1555123456@phone",
   405  		displayName:     "TEST",
   406  		displayLabel:    "+1555123456",
   407  		keybaseUsername: "",
   408  	}, pluckSearchResultForTest(res[1]))
   409  
   410  	// If we have a username match coming from the service, prefer it instead
   411  	// of contact result for the same user but with SBS assertion in it.
   412  	res, err = searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   413  		IncludeContacts: true,
   414  		Service:         "keybase",
   415  		Query:           "tuser",
   416  		MaxResults:      50,
   417  	})
   418  	require.NoError(t, err)
   419  	require.Equal(t, searchResultForTest{
   420  		id:              "tuser1",
   421  		displayName:     "tuser1",
   422  		displayLabel:    "Test User 123",
   423  		keybaseUsername: "tuser1",
   424  	}, pluckSearchResultForTest(res[0]))
   425  }
   426  
   427  func TestSearchContactDeduplicateNameAndLabel(t *testing.T) {
   428  	tc, searchHandler, _ := setupUserSearchTest(t)
   429  	defer tc.Cleanup()
   430  
   431  	contactlist := []keybase1.ProcessedContact{
   432  		makeContact(makeContactArg{index: 0, name: "Alice", email: "a@example.org", username: "alice"}),
   433  		makeContact(makeContactArg{index: 1, name: "Mary Elizabeth Smith", email: "smith@example.com", username: "keybasetester"}),
   434  		makeContact(makeContactArg{index: 2, name: "Mary Elizabeth Smith", email: "mary.smith@example.com", username: "keybasetester"}),
   435  		makeContact(makeContactArg{index: 3, name: "Mary Elizabeth Smith", phone: "+1555123456", username: "keybasetester"}),
   436  	}
   437  
   438  	err := tc.G.SyncedContactList.SaveProcessedContacts(tc.MetaContext(), contactlist)
   439  	require.NoError(t, err)
   440  
   441  	{
   442  		// Best match here is `smith@example.com` and we expect to see only that
   443  		// because they all resolve to same user.
   444  		res, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   445  			IncludeContacts: true,
   446  			Service:         "keybase",
   447  			Query:           "smith",
   448  			MaxResults:      50,
   449  		})
   450  		require.NoError(t, err)
   451  		require.Len(t, res, 1)
   452  		require.NotNil(t, res[0].Contact)
   453  		require.Equal(t, "[smith@example.com]@email", res[0].Contact.Assertion)
   454  	}
   455  
   456  	{
   457  		// But others are still findable
   458  		res, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   459  			IncludeContacts: true,
   460  			Service:         "keybase",
   461  			Query:           "555123456",
   462  			MaxResults:      50,
   463  		})
   464  		require.NoError(t, err)
   465  		require.Len(t, res, 1)
   466  		require.NotNil(t, res[0].Contact)
   467  		require.Equal(t, "1555123456@phone", res[0].Contact.Assertion)
   468  	}
   469  }
   470  
   471  func TestContactSearchMixing(t *testing.T) {
   472  	tc, searchHandler, searchProv := setupUserSearchTest(t)
   473  	defer tc.Cleanup()
   474  
   475  	contactlist := []keybase1.ProcessedContact{
   476  		makeContact(makeContactArg{index: 0, name: "Isaac Newton", phone: "+1555123456"}),
   477  		makeContact(makeContactArg{index: 1, name: "Pierre de Fermat", email: "fermatp@keyba.se", username: "pierre"}),
   478  		makeContact(makeContactArg{index: 2, name: "Gottfried Wilhelm Leibniz", phone: "+1555165432"}),
   479  	}
   480  
   481  	searchProv.addUser(testAddUserArg{username: "pierre"}) // the one we have in contacts
   482  	for i := 0; i < 5; i++ {
   483  		searchProv.addUser(testAddUserArg{fmt.Sprintf("isaac%d", i), fmt.Sprintf("The Isaac %d", i)})
   484  		// Longer names score lower
   485  		searchProv.addUser(testAddUserArg{fmt.Sprintf("isaac_____%d", i), fmt.Sprintf("The Isaac %d", i)})
   486  		searchProv.addUser(testAddUserArg{fmt.Sprintf("isaacsxzzz%d", i), fmt.Sprintf("The Isaac %d", i)})
   487  	}
   488  
   489  	err := tc.G.SyncedContactList.SaveProcessedContacts(tc.MetaContext(), contactlist)
   490  	require.NoError(t, err)
   491  
   492  	{
   493  		// Expecting to see our contact within the results. All the `isaacX`
   494  		// users will score higher than our contact, so it will come 6th on the
   495  		// list.
   496  		res, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   497  			IncludeContacts: true,
   498  			Service:         "keybase",
   499  			Query:           "isaac",
   500  			MaxResults:      10,
   501  		})
   502  		require.NoError(t, err)
   503  		require.Len(t, res, 10)
   504  		require.NotNil(t, res[5].Contact)
   505  		require.Equal(t, "1555123456@phone", res[5].Contact.Assertion)
   506  		require.Equal(t, "Isaac Newton", res[5].Contact.DisplayName)
   507  	}
   508  }
   509  
   510  func TestContactSearchMobilePhonesGoFirst(t *testing.T) {
   511  	tc, searchHandler, _ := setupUserSearchTest(t)
   512  	defer tc.Cleanup()
   513  
   514  	contactList := []keybase1.ProcessedContact{
   515  		makeContact(makeContactArg{index: 0, name: "Isaac Newton", phone: "+1555123456", label: "home"}),
   516  		makeContact(makeContactArg{index: 1, name: "Isaac Newton", email: "isaac@newt.on"}),
   517  		makeContact(makeContactArg{index: 2, name: "Isaac Newton", phone: "+1555012345", label: "mobile"}),
   518  	}
   519  
   520  	err := tc.G.SyncedContactList.SaveProcessedContacts(tc.MetaContext(), contactList)
   521  	require.NoError(t, err)
   522  
   523  	{
   524  		// We expect to see the "mobile" phone number ranked above the "home" phone
   525  		// number, and both should rank above email.
   526  		res, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   527  			IncludeContacts: true,
   528  			Service:         "keybase",
   529  			Query:           "isaac",
   530  			MaxResults:      10,
   531  		})
   532  		require.NoError(t, err)
   533  		require.Len(t, res, 3)
   534  		require.Equal(t, "1555012345@phone", res[0].Contact.Assertion)
   535  		require.Equal(t, "1555123456@phone", res[1].Contact.Assertion)
   536  		require.Equal(t, "[isaac@newt.on]@email", res[2].Contact.Assertion)
   537  	}
   538  }
   539  
   540  func TestUserSearchPhoneEmail(t *testing.T) {
   541  	tc, searchHandler, _ := setupUserSearchTest(t)
   542  	defer tc.Cleanup()
   543  
   544  	contactlist := []keybase1.ProcessedContact{
   545  		makeContact(makeContactArg{index: 1, name: "Pierre de Fermat", email: "fermatp@keyba.se", username: "pierre"}),
   546  		makeContact(makeContactArg{index: 2, name: "Gottfried Wilhelm Leibniz", phone: "+1555165432", username: "lwg"}),
   547  	}
   548  
   549  	err := tc.G.SyncedContactList.SaveProcessedContacts(tc.MetaContext(), contactlist)
   550  	require.NoError(t, err)
   551  
   552  	doSearch := func(service, query string) []keybase1.APIUserSearchResult {
   553  		res, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   554  			IncludeContacts: false,
   555  			Service:         service,
   556  			Query:           query,
   557  			MaxResults:      10,
   558  		})
   559  		require.NoError(t, err)
   560  		return res
   561  	}
   562  
   563  	{
   564  		// Imptofu searches (service "phone" or "email") do not search in
   565  		// synced contacts anymore.
   566  		query := "+1555165432"
   567  		res := doSearch("phone", query)
   568  		require.Len(t, res, 1)
   569  		require.NotNil(t, res[0].Imptofu)
   570  		require.Empty(t, res[0].Imptofu.KeybaseUsername)
   571  		require.Equal(t, "1555165432@phone", res[0].Imptofu.Assertion)
   572  	}
   573  
   574  	{
   575  		// Same with e-mail.
   576  		query := "fermatp@keyba.se"
   577  		res := doSearch("email", query)
   578  		require.Len(t, res, 1)
   579  		require.NotNil(t, res[0].Imptofu)
   580  		require.Empty(t, res[0].Imptofu.KeybaseUsername)
   581  		require.Equal(t, "[fermatp@keyba.se]@email", res[0].Imptofu.Assertion)
   582  	}
   583  
   584  	{
   585  		// Ask for a different number and get an imptofu result.
   586  		query := "+1201555201"
   587  		res := doSearch("phone", query)
   588  		require.Len(t, res, 1)
   589  		require.Nil(t, res[0].Contact)
   590  		require.NotNil(t, res[0].Imptofu)
   591  		require.Empty(t, res[0].Imptofu.KeybaseUsername)
   592  		require.Equal(t, "1201555201@phone", res[0].Imptofu.Assertion)
   593  		require.Empty(t, res[0].Imptofu.PrettyName)
   594  		require.Empty(t, res[0].Imptofu.Label)
   595  		require.Equal(t, "phone", res[0].Imptofu.AssertionKey)
   596  		require.Equal(t, "1201555201", res[0].Imptofu.AssertionValue)
   597  	}
   598  
   599  	{
   600  		// Imp tofu email.
   601  		query := "test@keyba.se"
   602  		res := doSearch("email", query)
   603  		require.Len(t, res, 1)
   604  		require.Nil(t, res[0].Contact)
   605  		require.NotNil(t, res[0].Imptofu)
   606  		require.Empty(t, res[0].Imptofu.KeybaseUsername)
   607  		require.Equal(t, "[test@keyba.se]@email", res[0].Imptofu.Assertion)
   608  		require.Empty(t, res[0].Imptofu.PrettyName)
   609  		require.Empty(t, res[0].Imptofu.Label)
   610  		require.Equal(t, "email", res[0].Imptofu.AssertionKey)
   611  		require.Equal(t, "test@keyba.se", res[0].Imptofu.AssertionValue)
   612  	}
   613  
   614  	{
   615  		// Email should be lowercased when returning search result.
   616  		query := "TEST@keyba.se"
   617  		res := doSearch("email", query)
   618  		require.Len(t, res, 1)
   619  		require.Nil(t, res[0].Contact)
   620  		require.NotNil(t, res[0].Imptofu)
   621  		require.Empty(t, res[0].Imptofu.KeybaseUsername)
   622  		// Assertion should be lowercased for display names.
   623  		require.Equal(t, "[test@keyba.se]@email", res[0].Imptofu.Assertion)
   624  		require.Empty(t, res[0].Imptofu.PrettyName)
   625  		require.Empty(t, res[0].Imptofu.Label)
   626  		require.Equal(t, "email", res[0].Imptofu.AssertionKey)
   627  		require.Equal(t, "test@keyba.se", res[0].Imptofu.AssertionValue)
   628  	}
   629  }
   630  
   631  func TestUserSearchBadArgs(t *testing.T) {
   632  	tc, searchHandler, _ := setupUserSearchTest(t)
   633  	defer tc.Cleanup()
   634  
   635  	// Invalid empty service name
   636  	_, err := searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   637  		IncludeContacts: false,
   638  		Service:         "",
   639  		Query:           "test",
   640  		MaxResults:      10,
   641  	})
   642  	require.Error(t, err)
   643  
   644  	// IncludeContacts=true with invalid `Service` (only "keybase" is allowed
   645  	// for IncludeContacts).
   646  	_, err = searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{
   647  		IncludeContacts: true,
   648  		Service:         "twitter",
   649  		Query:           "test",
   650  		MaxResults:      10,
   651  	})
   652  	require.Error(t, err)
   653  }
   654  
   655  func TestImptofuSearch(t *testing.T) {
   656  	tc := libkb.SetupTest(t, "usersearch", 1)
   657  	defer tc.Cleanup()
   658  
   659  	mockContactsProv := contacts.MakeMockProvider(t)
   660  	contactsProv := &contacts.CachedContactsProvider{
   661  		Provider: mockContactsProv,
   662  		Store:    contacts.NewContactCacheStore(tc.G),
   663  	}
   664  
   665  	searchHandler := NewUserSearchHandler(nil, tc.G, contactsProv)
   666  
   667  	mockContactsProv.PhoneNumbers["+48111222332"] = contacts.MakeMockLookupUser("alice", "Alice A")
   668  	mockContactsProv.Emails["bob@keyba.se"] = contacts.MakeMockLookupUser("bob", "Bobby")
   669  
   670  	ret, err := searchHandler.searchEmailsOrPhoneNumbers(tc.MetaContext(),
   671  		[]keybase1.EmailAddress{}, []keybase1.RawPhoneNumber{"+48111222332"},
   672  		true, true)
   673  
   674  	require.NoError(t, err)
   675  	require.Len(t, ret.emails, 0, "0 emails in results (we didn't ask)")
   676  	require.Len(t, ret.phoneNumbers, 1, "1 phone number in results")
   677  	phoneRet := ret.phoneNumbers[0]
   678  	require.Equal(t, phoneRet.input, "+48111222332")
   679  	require.Equal(t, phoneRet.assertion.String(), "48111222332@phone")
   680  	require.True(t, phoneRet.found)
   681  	require.True(t, phoneRet.UID.Exists())
   682  	require.Equal(t, phoneRet.username, "alice")
   683  	require.Equal(t, phoneRet.fullName, "Alice A")
   684  
   685  	ret, err = searchHandler.searchEmailsOrPhoneNumbers(tc.MetaContext(),
   686  		[]keybase1.EmailAddress{}, []keybase1.RawPhoneNumber{"+1555123456"},
   687  		true, true)
   688  
   689  	require.NoError(t, err)
   690  	require.Len(t, ret.emails, 0, "0 emails in results (we didn't ask)")
   691  	require.Len(t, ret.phoneNumbers, 1, "1 phone number in results (even if it wasn't found)")
   692  	phoneRet = ret.phoneNumbers[0]
   693  	require.Equal(t, phoneRet.input, "+1555123456")
   694  	require.Equal(t, phoneRet.assertion.String(), "1555123456@phone")
   695  	require.False(t, phoneRet.found)
   696  	require.True(t, phoneRet.UID.IsNil())
   697  	require.Empty(t, phoneRet.username)
   698  	require.Empty(t, phoneRet.fullName)
   699  
   700  	ret, err = searchHandler.searchEmailsOrPhoneNumbers(tc.MetaContext(),
   701  		[]keybase1.EmailAddress{"bob@keyba.se"}, []keybase1.RawPhoneNumber{},
   702  		true, true)
   703  
   704  	require.NoError(t, err)
   705  	require.Len(t, ret.emails, 1, "1 email in results")
   706  	require.Len(t, ret.phoneNumbers, 0, "0 phone numbers in results (we didn't ask)")
   707  	emailRet := ret.emails[0]
   708  	require.Equal(t, emailRet.input, "bob@keyba.se")
   709  	require.Equal(t, emailRet.assertion.String(), "[bob@keyba.se]@email")
   710  	require.True(t, emailRet.found)
   711  	require.True(t, emailRet.UID.Exists())
   712  	require.Equal(t, emailRet.username, "bob")
   713  	require.Equal(t, emailRet.fullName, "Bobby")
   714  
   715  	ret, err = searchHandler.searchEmailsOrPhoneNumbers(tc.MetaContext(),
   716  		[]keybase1.EmailAddress{"alice@keyba.se"}, []keybase1.RawPhoneNumber{},
   717  		true, true)
   718  
   719  	require.NoError(t, err)
   720  	require.Len(t, ret.emails, 1, "1 email in results")
   721  	require.Len(t, ret.phoneNumbers, 0, "0 phone numbers in results (we didn't ask)")
   722  	emailRet = ret.emails[0]
   723  	require.Equal(t, emailRet.input, "alice@keyba.se")
   724  	require.Equal(t, emailRet.assertion.String(), "[alice@keyba.se]@email")
   725  	require.False(t, emailRet.found)
   726  	require.True(t, emailRet.UID.IsNil())
   727  	require.Empty(t, emailRet.username)
   728  	require.Empty(t, emailRet.fullName)
   729  }
   730  
   731  func TestImptofuSearchMulti(t *testing.T) {
   732  	tc := libkb.SetupTest(t, "usersearch", 1)
   733  	defer tc.Cleanup()
   734  
   735  	mockContactsProv := contacts.MakeMockProvider(t)
   736  	contactsProv := &contacts.CachedContactsProvider{
   737  		Provider: mockContactsProv,
   738  		Store:    contacts.NewContactCacheStore(tc.G),
   739  	}
   740  
   741  	searchHandler := NewUserSearchHandler(nil, tc.G, contactsProv)
   742  
   743  	mockContactsProv.PhoneNumbers["+48111222332"] = contacts.MakeMockLookupUser("alice", "Alice A")
   744  	mockContactsProv.PhoneNumbers["+1123456789"] = contacts.MakeMockLookupUser("lily", "")
   745  	mockContactsProv.Emails["bobby6@example.com"] = contacts.MakeMockLookupUser("bob", "Bobby")
   746  	mockContactsProv.Emails["bob@keyba.se"] = contacts.MakeMockLookupUser("bob", "Bobby")
   747  
   748  	ret, err := searchHandler.searchEmailsOrPhoneNumbers(tc.MetaContext(),
   749  		[]keybase1.EmailAddress{"bobby6@example.com", "bob@keyba.se", "alice@keyba.se", "alice"},
   750  		[]keybase1.RawPhoneNumber{"+48111222332", "+1123456789", "+44123123", "011"},
   751  		true, true)
   752  
   753  	require.NoError(t, err)
   754  
   755  	// Number of results is always equal to the number of inputs.
   756  	require.Len(t, ret.emails, 4)
   757  	require.Len(t, ret.phoneNumbers, 4)
   758  
   759  	for i, v := range ret.emails {
   760  		if i < 2 {
   761  			// "bobby6@example.com", "bob@keyba.se"
   762  			require.True(t, v.validInput)
   763  			require.NotNil(t, v.assertion)
   764  			require.True(t, v.found)
   765  			require.True(t, v.UID.Exists())
   766  			require.Equal(t, v.username, "bob")
   767  			require.Equal(t, v.fullName, "Bobby")
   768  		} else if i == 2 {
   769  			// "alice@keyba.se"
   770  			require.True(t, v.validInput)
   771  			require.NotNil(t, v.assertion)
   772  			require.False(t, v.found)
   773  			require.True(t, v.UID.IsNil())
   774  		} else if i == 3 {
   775  			// "alice"
   776  			require.False(t, v.validInput)
   777  			require.Nil(t, v.assertion)
   778  			require.False(t, v.found)
   779  			require.True(t, v.UID.IsNil())
   780  		}
   781  	}
   782  
   783  	for i, v := range ret.phoneNumbers {
   784  		if i < 2 {
   785  			require.True(t, v.validInput)
   786  			require.NotNil(t, v.assertion)
   787  			require.True(t, v.found)
   788  			require.True(t, v.UID.Exists())
   789  			switch i {
   790  			case 0:
   791  				// "+48111222332", "+1123456789"
   792  				require.Equal(t, v.username, "alice")
   793  				require.Equal(t, v.fullName, "Alice A")
   794  			case 1:
   795  				require.Equal(t, v.username, "lily")
   796  				require.Equal(t, v.fullName, "")
   797  			}
   798  		} else if i == 2 {
   799  			// "+44123123"
   800  			require.True(t, v.validInput)
   801  			require.NotNil(t, v.assertion)
   802  			require.False(t, v.found)
   803  			require.True(t, v.UID.IsNil())
   804  		} else if i == 3 {
   805  			// "011"
   806  			require.False(t, v.validInput)
   807  			require.Nil(t, v.assertion)
   808  			require.False(t, v.found)
   809  			require.True(t, v.UID.IsNil())
   810  		}
   811  	}
   812  }
   813  
   814  func TestImptofuBadInput(t *testing.T) {
   815  	tc := libkb.SetupTest(t, "usersearch", 1)
   816  	defer tc.Cleanup()
   817  
   818  	mockContactsProv := contacts.MakeMockProvider(t)
   819  	contactsProv := &contacts.CachedContactsProvider{
   820  		Provider: mockContactsProv,
   821  		Store:    contacts.NewContactCacheStore(tc.G),
   822  	}
   823  
   824  	searchHandler := NewUserSearchHandler(nil, tc.G, contactsProv)
   825  
   826  	ret, err := searchHandler.searchEmailsOrPhoneNumbers(tc.MetaContext(),
   827  		[]keybase1.EmailAddress{"alice"}, []keybase1.RawPhoneNumber{"test", "01234", "+1"},
   828  		true, true)
   829  
   830  	require.NoError(t, err)
   831  
   832  	require.Len(t, ret.emails, 1)
   833  	require.Len(t, ret.phoneNumbers, 3)
   834  	require.Equal(t, ret.emails[0].input, "alice")
   835  	require.Equal(t, ret.phoneNumbers[0].input, "test")
   836  	require.Equal(t, ret.phoneNumbers[1].input, "01234")
   837  	require.Equal(t, ret.phoneNumbers[2].input, "+1")
   838  
   839  	all := ret.emails
   840  	all = append(all, ret.phoneNumbers...)
   841  	for _, v := range all {
   842  		require.Equal(t, v.validInput, false)
   843  		require.Nil(t, v.assertion)
   844  		require.False(t, v.found)
   845  		require.True(t, v.UID.IsNil())
   846  		require.Empty(t, v.username)
   847  		require.Empty(t, v.fullName)
   848  	}
   849  }
   850  
   851  func TestBulkEmailSearch(t *testing.T) {
   852  	tc := libkb.SetupTest(t, "usersearch", 1)
   853  	defer tc.Cleanup()
   854  
   855  	mockContactsProv := contacts.MakeMockProvider(t)
   856  	contactsProv := &contacts.CachedContactsProvider{
   857  		Provider: mockContactsProv,
   858  		Store:    contacts.NewContactCacheStore(tc.G),
   859  	}
   860  
   861  	searchHandler := NewUserSearchHandler(nil, tc.G, contactsProv)
   862  
   863  	emails := []string{
   864  		"alice@example.org",
   865  		"bob@example.com",
   866  		"no-reply@keybase.example.com",
   867  		"test@example.edu",
   868  		"hello@keybase.example.com",
   869  	}
   870  	separators := []string{
   871  		",", "\n", ", ", "\r\n",
   872  	}
   873  
   874  	query := ""
   875  	for i, v := range emails {
   876  		query += v
   877  		if i < len(emails)-1 {
   878  			query += separators[i%len(separators)]
   879  		}
   880  	}
   881  
   882  	ret, err := searchHandler.BulkEmailOrPhoneSearch(context.Background(), keybase1.BulkEmailOrPhoneSearchArg{
   883  		Emails: query,
   884  	})
   885  
   886  	require.NoError(t, err)
   887  	require.Len(t, ret, len(emails))
   888  	for i, v := range ret {
   889  		require.Equal(t, v.Assertion, fmt.Sprintf("[%s]@email", emails[i]))
   890  		require.Equal(t, v.AssertionKey, "email")
   891  		require.Equal(t, v.AssertionValue, emails[i])
   892  
   893  		require.False(t, v.FoundUser)
   894  		require.Empty(t, v.Username)
   895  		require.Empty(t, v.FullName)
   896  	}
   897  
   898  	ret, err = searchHandler.BulkEmailOrPhoneSearch(context.Background(), keybase1.BulkEmailOrPhoneSearchArg{
   899  		Emails: "Alice <alice@example.com>,Bob <bob@example.com>",
   900  	})
   901  
   902  	require.NoError(t, err)
   903  	require.Len(t, ret, 2)
   904  	require.Equal(t, ret[0].Input, "alice@example.com")
   905  	require.Equal(t, ret[0].Assertion, "[alice@example.com]@email")
   906  	require.Equal(t, ret[1].Input, "bob@example.com")
   907  	require.Equal(t, ret[1].Assertion, "[bob@example.com]@email")
   908  }
   909  
   910  func TestBulkEmailSearchBadInput(t *testing.T) {
   911  	tc := libkb.SetupTest(t, "usersearch", 1)
   912  	defer tc.Cleanup()
   913  
   914  	mockContactsProv := contacts.MakeMockProvider(t)
   915  	contactsProv := &contacts.CachedContactsProvider{
   916  		Provider: mockContactsProv,
   917  		Store:    contacts.NewContactCacheStore(tc.G),
   918  	}
   919  
   920  	searchHandler := NewUserSearchHandler(nil, tc.G, contactsProv)
   921  
   922  	emails := "\nalice:,alice@example.org, alice, x\n,  ,\n"
   923  	ret, err := searchHandler.BulkEmailOrPhoneSearch(context.Background(), keybase1.BulkEmailOrPhoneSearchArg{
   924  		Emails: emails,
   925  	})
   926  
   927  	require.NoError(t, err)
   928  	require.Len(t, ret, 1) // there was only one valid email in there
   929  	require.Equal(t, ret[0].Input, "alice@example.org")
   930  	require.Equal(t, ret[0].Assertion, "[alice@example.org]@email")
   931  	require.Equal(t, ret[0].AssertionValue, "alice@example.org")
   932  	require.Equal(t, ret[0].AssertionKey, "email")
   933  	require.Equal(t, ret[0].FoundUser, false)
   934  	require.Equal(t, ret[0].Username, "")
   935  	require.Equal(t, ret[0].FullName, "")
   936  
   937  	ret, err = searchHandler.BulkEmailOrPhoneSearch(context.Background(), keybase1.BulkEmailOrPhoneSearchArg{
   938  		PhoneNumbers: []keybase1.PhoneNumber{"+1", "00"},
   939  	})
   940  
   941  	require.NoError(t, err)
   942  	require.Len(t, ret, 0)
   943  }