github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/service/contacts_test.go (about) 1 package service 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/keybase/client/go/contacts" 8 "github.com/keybase/client/go/kbtest" 9 "github.com/keybase/client/go/libkb" 10 keybase1 "github.com/keybase/client/go/protocol/keybase1" 11 "github.com/keybase/clockwork" 12 "github.com/stretchr/testify/require" 13 "golang.org/x/net/context" 14 ) 15 16 // Test "end-to-end" syncing contact (by using SaveContactList RPC with raw 17 // contact list) and interacting with it using UserSearch RPCs. 18 19 // Mock contact provider. 20 21 type contactSyncTest struct { 22 searchHandler *UserSearchHandler 23 contactsHandler *ContactsHandler 24 contactsMock *contacts.MockContactsProvider 25 searchMock *testUserSearchProvider 26 user *kbtest.FakeUser 27 28 contactsCache *contacts.CachedContactsProvider 29 } 30 31 func setupContactSyncTest(t *testing.T) (tc libkb.TestContext, test contactSyncTest) { 32 tc = libkb.SetupTest(t, "contacts", 3) 33 tc.G.SyncedContactList = contacts.NewSavedContactsStore(tc.G) 34 35 user, err := kbtest.CreateAndSignupFakeUser("lmu", tc.G) 36 require.NoError(t, err) 37 38 mockContactsProv := contacts.MakeMockProvider(t) 39 contactsProv := &contacts.CachedContactsProvider{ 40 Provider: mockContactsProv, 41 Store: contacts.NewContactCacheStore(tc.G), 42 } 43 searchHandler := NewUserSearchHandler(nil, tc.G, contactsProv) 44 searchProv := &testUserSearchProvider{T: t} 45 searchHandler.searchProvider = searchProv 46 47 contactsHandler := NewContactsHandler(nil, tc.G, contactsProv) 48 return tc, contactSyncTest{ 49 searchHandler: searchHandler, 50 contactsHandler: contactsHandler, 51 contactsMock: mockContactsProv, 52 searchMock: searchProv, 53 user: user, 54 contactsCache: contactsProv, 55 } 56 } 57 58 func (c *contactSyncTest) clearCache(mctx libkb.MetaContext) error { 59 return c.contactsCache.Store.ClearCache(mctx) 60 } 61 62 func TestContactSyncAndSearch(t *testing.T) { 63 tc, all := setupContactSyncTest(t) 64 defer tc.Cleanup() 65 66 clock := clockwork.NewFakeClock() 67 tc.G.SetClock(clock) 68 69 all.searchMock.addUser(testAddUserArg{username: "alice2"}) 70 71 rawContacts := []keybase1.Contact{ 72 contacts.MakeContact("Alice A", 73 contacts.MakePhoneComponent("mobile", "+48111222332"), 74 ), 75 contacts.MakeContact("Alice A", 76 contacts.MakePhoneComponent("mobile", "+48111222333"), 77 contacts.MakeEmailComponent("email", "alice@example.org"), 78 ), 79 } 80 81 // Phone component from rawContacts will resolve to user `alice` but e-mail 82 // will not. We are expecting to be able to search for both, but not see 83 // both in user recommendations. 84 85 _, err := all.contactsHandler.SaveContactList(context.Background(), keybase1.SaveContactListArg{ 86 Contacts: rawContacts, 87 }) 88 require.NoError(t, err) 89 90 // bust cache, new resolution should be returned 91 clock.Advance(72 * time.Hour) 92 all.contactsMock.PhoneNumbers["+48111222332"] = contacts.MakeMockLookupUser("alice", "") 93 all.contactsMock.PhoneNumbers["+48111222333"] = contacts.MakeMockLookupUser("alice", "") 94 result, err := all.contactsHandler.SaveContactList(context.Background(), keybase1.SaveContactListArg{ 95 Contacts: rawContacts, 96 }) 97 require.NoError(t, err) 98 99 newlyResolved := result.NewlyResolved 100 // We should only have 1 resolved, since we dedupe. 101 require.Len(t, newlyResolved, 1) 102 require.Equal(t, newlyResolved[0].ContactName, "Alice A") 103 require.Equal(t, newlyResolved[0].Username, "alice") 104 require.Equal(t, newlyResolved[0].Assertion, "48111222333@phone") 105 106 { 107 // Try raw contact list lookup. 108 list, err := all.contactsHandler.LookupSavedContactsList(context.Background(), 0) 109 require.NoError(t, err) 110 // We have two contacts with three components between them 111 require.Len(t, list, 3) 112 } 113 114 { 115 // We expect one "Alice A" show up as resolved. She should be the second one. 116 list, err := all.contactsHandler.GetContactsForUserRecommendations(context.Background(), 0) 117 require.NoError(t, err) 118 require.Len(t, list, 1) 119 require.Equal(t, "alice", list[0].DisplayName) 120 require.Equal(t, "Alice A", list[0].DisplayLabel) 121 require.Equal(t, "alice", list[0].Username) 122 require.NotNil(t, list[0].Component.PhoneNumber) 123 require.Equal(t, "48111222332@phone", list[0].Assertion) 124 } 125 126 { 127 // When searching for "alice" in contacts, contact comes first because 128 // it's a better match than `alice2` user. 129 130 // NOTE: This test is very unrealistic because contact resolves to user 131 // `alice` but that user is not provided by mocked search provider. If 132 // it was, Keybase result would have precedence, and the first result 133 // would not be SBS contact result. 134 res, err := all.searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{ 135 IncludeContacts: true, 136 Service: "keybase", 137 Query: "alice", 138 MaxResults: 50, 139 }) 140 require.NoError(t, err) 141 require.Len(t, res, 2) 142 pres := pluckAllSearchResultForTest(res) 143 require.Equal(t, "48111222332@phone", pres[0].id) 144 require.Equal(t, "alice", pres[0].keybaseUsername) 145 require.Equal(t, "alice2", pres[1].id) 146 require.Equal(t, "alice2", pres[1].keybaseUsername) 147 } 148 149 { 150 // Should be possible to search for both alice's components - as long a 151 // search don't yield both at the same time, then it's handled like in 152 // the previous case. 153 res, err := all.searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{ 154 IncludeContacts: true, 155 Service: "keybase", 156 Query: "111222333", 157 MaxResults: 50, 158 }) 159 require.NoError(t, err) 160 require.Len(t, res, 1) 161 require.NotNil(t, res[0].Contact) 162 pres := pluckSearchResultForTest(res[0]) 163 require.Equal(t, "48111222333@phone", pres.id) 164 require.Equal(t, "alice", pres.displayName) 165 require.Equal(t, "+48111222333", pres.displayLabel) 166 167 res, err = all.searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{ 168 IncludeContacts: true, 169 Service: "keybase", 170 Query: "alice@example.org", 171 MaxResults: 50, 172 }) 173 require.NoError(t, err) 174 require.Len(t, res, 1) 175 require.NotNil(t, res[0].Contact) 176 pres = pluckSearchResultForTest(res[0]) 177 require.Equal(t, "[alice@example.org]@email", pres.id) 178 require.Equal(t, "Alice A", pres.displayName) 179 require.Equal(t, "alice@example.org (email)", pres.displayLabel) 180 } 181 } 182 183 func TestContactShouldFilterOutSelf(t *testing.T) { 184 tc, all := setupContactSyncTest(t) 185 defer tc.Cleanup() 186 187 all.searchMock.addUser(testAddUserArg{username: "alice2"}) 188 189 all.contactsMock.PhoneNumbers["+1555222"] = contacts.MakeMockLookupUser(all.user.Username, "") 190 191 rawContacts := []keybase1.Contact{ 192 contacts.MakeContact("Alice A", 193 contacts.MakePhoneComponent("mobile", "+48111222333"), 194 contacts.MakeEmailComponent("email", "alice@example.org"), 195 ), 196 contacts.MakeContact("Charlie", 197 contacts.MakePhoneComponent("mobile", "+1555222"), 198 ), 199 } 200 201 _, err := all.contactsHandler.SaveContactList(context.Background(), keybase1.SaveContactListArg{ 202 Contacts: rawContacts, 203 }) 204 require.NoError(t, err) 205 206 list, err := all.contactsHandler.GetContactsForUserRecommendations(context.Background(), 0) 207 require.NoError(t, err) 208 require.Len(t, list, 2) 209 for _, v := range list { 210 // Only first contact entries should be there 211 require.Equal(t, "Alice A", v.ContactName) 212 require.Equal(t, 0, v.ContactIndex) 213 } 214 } 215 216 func TestRecommendationsPreferEmail(t *testing.T) { 217 tc, all := setupContactSyncTest(t) 218 defer tc.Cleanup() 219 220 all.contactsMock.PhoneNumbers["+48111222333"] = contacts.MakeMockLookupUser("alice", "") 221 all.contactsMock.Emails["alice@example.org"] = contacts.MakeMockLookupUser("alice", "") 222 223 rawContacts := []keybase1.Contact{ 224 contacts.MakeContact("Alice A", 225 contacts.MakePhoneComponent("mobile", "+48111222333"), 226 contacts.MakeEmailComponent("email", "alice@example.org"), 227 ), 228 } 229 230 _, err := all.contactsHandler.SaveContactList(context.Background(), keybase1.SaveContactListArg{ 231 Contacts: rawContacts, 232 }) 233 require.NoError(t, err) 234 235 list, err := all.contactsHandler.GetContactsForUserRecommendations(context.Background(), 0) 236 require.NoError(t, err) 237 require.Len(t, list, 1) 238 require.True(t, list[0].Resolved) 239 require.Equal(t, "alice", list[0].Username) 240 require.Equal(t, "[alice@example.org]@email", list[0].Assertion) 241 } 242 243 func TestDuplicateContactAssertions(t *testing.T) { 244 tc, all := setupContactSyncTest(t) 245 defer tc.Cleanup() 246 247 rawContacts := []keybase1.Contact{ 248 contacts.MakeContact("Alice A", 249 contacts.MakePhoneComponent("mobile", "+48111222333"), 250 ), 251 contacts.MakeContact("Mom", 252 contacts.MakePhoneComponent("mobile", "+48111222333"), 253 ), 254 } 255 256 _, err := all.contactsHandler.SaveContactList(context.Background(), keybase1.SaveContactListArg{ 257 Contacts: rawContacts, 258 }) 259 require.NoError(t, err) 260 261 { 262 // We expect to see both contacts here. 263 res, err := all.contactsHandler.GetContactsForUserRecommendations(context.Background(), 0) 264 require.NoError(t, err) 265 require.Len(t, res, 2) 266 } 267 268 { 269 // Same when searching 270 res, err := all.searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{ 271 IncludeContacts: true, 272 Service: "keybase", 273 Query: "111", 274 MaxResults: 50, 275 }) 276 require.NoError(t, err) 277 require.Len(t, res, 2) 278 } 279 280 { 281 // Make the number resolvable, re-import contacts. 282 all.contactsMock.PhoneNumbers["+48111222333"] = contacts.MakeMockLookupUser("alice", "A. Alice") 283 284 require.NoError(t, all.clearCache(tc.MetaContext())) 285 _, err := all.contactsHandler.SaveContactList(context.Background(), keybase1.SaveContactListArg{ 286 Contacts: rawContacts, 287 }) 288 require.NoError(t, err) 289 } 290 291 { 292 // We expect to see only one result here. Second contact is filtered out 293 // because of username deduplication. 294 res, err := all.contactsHandler.GetContactsForUserRecommendations(context.Background(), 0) 295 require.NoError(t, err) 296 require.Len(t, res, 1) 297 require.Equal(t, "alice", res[0].DisplayName) 298 require.Equal(t, "Alice A", res[0].DisplayLabel) 299 require.Equal(t, "48111222333@phone", res[0].Assertion) 300 } 301 302 { 303 // Only one contact when searching as well 304 res, err := all.searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{ 305 IncludeContacts: true, 306 Service: "keybase", 307 Query: "111", 308 MaxResults: 50, 309 }) 310 require.NoError(t, err) 311 require.Len(t, res, 1) 312 pres := pluckSearchResultForTest(res[0]) 313 require.Equal(t, "48111222333@phone", pres.id) 314 require.Equal(t, "alice", pres.displayName) 315 // Selecting query match to display label is still at play here. 316 require.Equal(t, "+48111222333", pres.displayLabel) 317 } 318 } 319 320 func TestSyncContactsWithServiceSummary(t *testing.T) { 321 tc, all := setupContactSyncTest(t) 322 defer tc.Cleanup() 323 324 aliceMock := contacts.MakeMockLookupUser("alice", "Alice Keybase") 325 aliceMock.ServiceMap = make(libkb.UserServiceSummary) 326 aliceMock.ServiceMap["rooter"] = "alice123" 327 aliceMock.ServiceMap["twitter"] = "tacovontaco" 328 329 all.contactsMock.Emails["alice@keyba.se"] = aliceMock 330 all.contactsMock.Emails["bob@keyba.se"] = contacts.MakeMockLookupUser("bob", "Bob Keybase") 331 332 rawContacts := []keybase1.Contact{ 333 contacts.MakeContact("Alice", 334 contacts.MakeEmailComponent("email", "alice@keyba.se"), 335 ), 336 contacts.MakeContact("Bob", 337 contacts.MakeEmailComponent("email", "bob@keyba.se"), 338 ), 339 } 340 341 _, err := all.contactsHandler.SaveContactList(context.Background(), keybase1.SaveContactListArg{ 342 Contacts: rawContacts, 343 }) 344 require.NoError(t, err) 345 346 res, err := all.searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{ 347 IncludeContacts: true, 348 Service: "keybase", 349 Query: "keyba.se", 350 MaxResults: 50, 351 }) 352 require.NoError(t, err) 353 require.Len(t, res, 2) 354 // Bob (no service map because provider returned no services) 355 require.NotNil(t, res[0].Contact) 356 require.Equal(t, "Bob", res[0].Contact.ContactName) 357 require.Nil(t, res[0].Contact.ServiceMap) 358 // Alice (with service map) 359 require.NotNil(t, res[1].Contact) 360 require.Equal(t, "Alice", res[1].Contact.ContactName) 361 require.NotNil(t, res[1].Contact.ServiceMap) 362 require.Len(t, res[1].Contact.ServiceMap, 2) 363 require.Equal(t, "alice123", res[1].Contact.ServiceMap["rooter"]) 364 require.Equal(t, "tacovontaco", res[1].Contact.ServiceMap["twitter"]) 365 } 366 367 func TestUserSearchPhoneEmailWithResults(t *testing.T) { 368 tc, all := setupContactSyncTest(t) 369 defer tc.Cleanup() 370 371 all.contactsMock.Emails["fermatp@keyba.se"] = contacts.MakeMockLookupUser("pierre", "Pierre de Fermat") 372 all.contactsMock.PhoneNumbers["+1555165432"] = contacts.MakeMockLookupUser("lwg", "Gottfried Wilhelm Leibniz") 373 374 doSearch := func(service, query string) []keybase1.APIUserSearchResult { 375 res, err := all.searchHandler.UserSearch(context.Background(), keybase1.UserSearchArg{ 376 IncludeContacts: false, 377 Service: service, 378 Query: query, 379 MaxResults: 10, 380 }) 381 require.NoError(t, err) 382 return res 383 } 384 385 // Do imptofu queries such that they will resolve when looked up by 386 // contacts provider. 387 388 { 389 query := "+1555165432" 390 res := doSearch("phone", query) 391 require.Len(t, res, 1) 392 require.NotNil(t, res[0].Imptofu) 393 require.Equal(t, "1555165432@phone", res[0].Imptofu.Assertion) 394 require.Equal(t, "lwg", res[0].Imptofu.KeybaseUsername) 395 require.Equal(t, "Gottfried Wilhelm Leibniz", res[0].Imptofu.PrettyName) 396 } 397 398 { 399 query := "fermatp@keyba.se" 400 res := doSearch("email", query) 401 require.Len(t, res, 1) 402 require.NotNil(t, res[0].Imptofu) 403 require.Equal(t, "[fermatp@keyba.se]@email", res[0].Imptofu.Assertion) 404 require.Equal(t, "pierre", res[0].Imptofu.KeybaseUsername) 405 require.Equal(t, "Pierre de Fermat", res[0].Imptofu.PrettyName) 406 } 407 }