github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libkbfs/favorites_test.go (about) 1 // Copyright 2016 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libkbfs 6 7 import ( 8 "reflect" 9 "testing" 10 "time" 11 12 "github.com/golang/mock/gomock" 13 "github.com/keybase/client/go/kbfs/favorites" 14 "github.com/keybase/client/go/kbfs/idutil" 15 "github.com/keybase/client/go/kbfs/kbfsedits" 16 "github.com/keybase/client/go/kbfs/tlf" 17 kbname "github.com/keybase/client/go/kbun" 18 "github.com/keybase/client/go/protocol/keybase1" 19 "github.com/stretchr/testify/require" 20 "golang.org/x/net/context" 21 ) 22 23 func favToAddToKBFolder(toAdd favorites.ToAdd) keybase1.Folder { 24 handle := toAdd.ToKBFolderHandle() 25 return keybase1.Folder{ 26 Private: handle.FolderType != keybase1.FolderType_PUBLIC, 27 Name: handle.Name, 28 FolderType: handle.FolderType, 29 Created: handle.Created, 30 } 31 } 32 33 func favTestInit(t *testing.T, testingDiskCache bool) ( 34 mockCtrl *gomock.Controller, config *ConfigMock, ctx context.Context) { 35 ctr := NewSafeTestReporter(t) 36 mockCtrl = gomock.NewController(ctr) 37 config = NewConfigMock(mockCtrl, ctr) 38 if !testingDiskCache { 39 config.mockKbpki.EXPECT().GetCurrentSession(gomock. 40 Any()).AnyTimes(). 41 Return(idutil.SessionInfo{ 42 Name: kbname.NormalizedUsername("tester"), 43 UID: keybase1.MakeTestUID(16), 44 }, nil) 45 } 46 config.mockClock.EXPECT().Now().Return(time.Unix(0, 0)).AnyTimes() 47 config.mockRep.EXPECT(). 48 NotifyFavoritesChanged(gomock.Any()).Return().AnyTimes() 49 50 return mockCtrl, config, context.Background() 51 } 52 53 func favTestShutdown(t *testing.T, mockCtrl *gomock.Controller, 54 config *ConfigMock, f *Favorites) { 55 if err := f.Shutdown(); err != nil { 56 t.Errorf("Couldn't shut down favorites: %v", err) 57 } 58 config.ctr.CheckForFailures() 59 mockCtrl.Finish() 60 } 61 62 func TestFavoritesAddTwice(t *testing.T) { 63 mockCtrl, config, ctx := favTestInit(t, false) 64 f := NewFavorites(config) 65 f.InitForTest() 66 defer favTestShutdown(t, mockCtrl, config, f) 67 68 // Call Add twice in a row, but only get one Add KBPKI call 69 fav1 := favorites.ToAdd{ 70 Folder: favorites.Folder{ 71 Name: "test", 72 Type: tlf.Public, 73 }, 74 Data: favorites.Data{}, 75 Created: false, 76 } 77 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), fav1.ToKBFolderHandle()). 78 Return(nil) 79 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()). 80 Return(keybase1.FavoritesResult{ 81 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav1)}, 82 }, nil) 83 if err := f.Add(ctx, fav1); err != nil { 84 t.Fatalf("Couldn't add favorite: %v", err) 85 } 86 87 // A second add shouldn't result in a KBPKI call 88 if err := f.Add(ctx, fav1); err != nil { 89 t.Fatalf("Couldn't re-add same favorite: %v", err) 90 } 91 } 92 93 func TestFavoriteAddCreatedAlwaysGoThrough(t *testing.T) { 94 mockCtrl, config, ctx := favTestInit(t, false) 95 f := NewFavorites(config) 96 f.InitForTest() 97 defer favTestShutdown(t, mockCtrl, config, f) 98 99 fav1 := favorites.ToAdd{ 100 Folder: favorites.Folder{ 101 Name: "test", 102 Type: tlf.Public, 103 }, 104 Data: favorites.Data{}, 105 Created: false, 106 } 107 expected1 := keybase1.FolderHandle{ 108 Name: "test", 109 FolderType: keybase1.FolderType_PUBLIC, 110 Created: false, 111 } 112 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), expected1).Return(nil) 113 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1.FavoritesResult{ 114 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav1)}, 115 }, nil) 116 if err := f.Add(ctx, fav1); err != nil { 117 t.Fatalf("Couldn't add favorite: %v", err) 118 } 119 120 fav2 := favorites.ToAdd{ 121 Folder: favorites.Folder{ 122 Name: "test", 123 Type: tlf.Public, 124 }, 125 Data: favorites.Data{}, 126 Created: true, 127 } 128 expected2 := keybase1.FolderHandle{ 129 Name: "test", 130 FolderType: keybase1.FolderType_PUBLIC, 131 Created: true, 132 } 133 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), expected2).Return(nil) 134 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1.FavoritesResult{ 135 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav2)}, 136 }, nil) 137 if err := f.Add(ctx, fav2); err != nil { 138 t.Fatalf("Couldn't add favorite: %v", err) 139 } 140 } 141 142 func TestFavoritesAddCreated(t *testing.T) { 143 mockCtrl, config, ctx := favTestInit(t, false) 144 f := NewFavorites(config) 145 f.InitForTest() 146 defer favTestShutdown(t, mockCtrl, config, f) 147 148 // Call Add with created = true 149 fav1 := favorites.ToAdd{ 150 Folder: favorites.Folder{ 151 Name: "test", 152 Type: tlf.Public, 153 }, 154 Data: favorites.Data{}, 155 Created: true, 156 } 157 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1.FavoritesResult{}, nil) 158 expected := keybase1.FolderHandle{ 159 Name: "test", 160 FolderType: keybase1.FolderType_PUBLIC, 161 Created: true, 162 } 163 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), expected). 164 Return(nil) 165 if err := f.Add(ctx, fav1); err != nil { 166 t.Fatalf("Couldn't add favorite: %v", err) 167 } 168 } 169 170 func TestFavoritesAddRemoveAdd(t *testing.T) { 171 mockCtrl, config, ctx := favTestInit(t, false) 172 f := NewFavorites(config) 173 f.InitForTest() 174 defer favTestShutdown(t, mockCtrl, config, f) 175 176 fav1 := favorites.ToAdd{ 177 Folder: favorites.Folder{ 178 Name: "test", 179 Type: tlf.Public, 180 }, 181 Data: favorites.Data{}, 182 Created: false, 183 } 184 185 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), fav1.ToKBFolderHandle()). 186 Return(nil) 187 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1.FavoritesResult{ 188 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav1)}, 189 }, nil) 190 if err := f.Add(ctx, fav1); err != nil { 191 t.Fatalf("Couldn't add favorite: %v", err) 192 } 193 194 config.mockKbpki.EXPECT().FavoriteDelete(gomock.Any(), fav1.ToKBFolderHandle()). 195 Return(nil) 196 if err := f.Delete(ctx, fav1.Folder); err != nil { 197 t.Fatalf("Couldn't delete favorite: %v", err) 198 } 199 200 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), fav1.ToKBFolderHandle()). 201 Return(nil) 202 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1.FavoritesResult{ 203 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav1)}, 204 }, nil) 205 config.mockKbfs.EXPECT().RefreshEditHistory(gomock.Any()).Return() 206 if err := f.Add(ctx, fav1); err != nil { 207 t.Fatalf("Couldn't re-add same favorite: %v", err) 208 } 209 } 210 211 func TestFavoritesAddAsync(t *testing.T) { 212 mockCtrl, config, ctx := favTestInit(t, false) 213 // Only one task at a time 214 f := newFavoritesWithChan(config, make(chan *favReq, 1)) 215 f.InitForTest() 216 defer favTestShutdown(t, mockCtrl, config, f) 217 218 // Call Add twice in a row, but only get one Add KBPKI call 219 fav1 := favorites.ToAdd{ 220 Folder: favorites.Folder{ 221 Name: "test", 222 Type: tlf.Public, 223 }, 224 Data: favorites.Data{}, 225 Created: false, 226 } 227 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1.FavoritesResult{}, nil) 228 229 c := make(chan struct{}) 230 // Block until there are multiple outstanding calls 231 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), fav1.ToKBFolderHandle()). 232 Do(func(_ context.Context, _ keybase1.FolderHandle) { 233 <-c 234 }).Return(nil) 235 236 // There should only be one FavoriteAdd call for all of these, and 237 // none of them should block. 238 f.AddAsync(ctx, fav1) 239 f.AddAsync(ctx, fav1) 240 f.AddAsync(ctx, fav1) 241 f.AddAsync(ctx, fav1) 242 f.AddAsync(ctx, fav1) 243 c <- struct{}{} 244 } 245 246 func TestFavoritesListFailsDuringAddAsync(t *testing.T) { 247 mockCtrl, config, ctx := favTestInit(t, false) 248 // Only one task at a time 249 f := newFavoritesWithChan(config, make(chan *favReq, 1)) 250 f.InitForTest() 251 defer favTestShutdown(t, mockCtrl, config, f) 252 253 // Call Add twice in a row, but only get one Add KBPKI call 254 fav1 := favorites.ToAdd{ 255 Folder: favorites.Folder{ 256 Name: "test", 257 Type: tlf.Public, 258 }, 259 Data: favorites.Data{}, 260 Created: false, 261 } 262 263 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), fav1.ToKBFolderHandle()).Return(nil) 264 // Cancel the first list request 265 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1. 266 FavoritesResult{}, context.Canceled) 267 268 f.AddAsync(ctx, fav1) // this will fail 269 // Wait so the next one doesn't get batched together with this one 270 if err := f.wg.Wait(context.Background()); err != nil { 271 t.Fatalf("Couldn't wait on favorites: %v", err) 272 } 273 274 // Now make sure the second time around, the favorites get listed 275 // and one gets added, even if its context gets added 276 c := make(chan struct{}) 277 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1.FavoritesResult{}, nil) 278 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), fav1.ToKBFolderHandle()). 279 Do(func(_ context.Context, _ keybase1.FolderHandle) { 280 c <- struct{}{} 281 }).Return(nil) 282 283 f.AddAsync(ctx, fav1) // should work 284 <-c 285 } 286 287 func TestFavoritesControlUserHistory(t *testing.T) { 288 mockCtrl, config, ctx := favTestInit(t, false) 289 f := NewFavorites(config) 290 f.Initialize(ctx) 291 defer favTestShutdown(t, mockCtrl, config, f) 292 293 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), gomock.Any()). 294 Return(nil) 295 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()). 296 Return(keybase1.FavoritesResult{}, nil) 297 config.mockKbs.EXPECT().EncryptFavorites(gomock.Any(), 298 gomock.Any()).Return(nil, nil) 299 config.mockCodec.EXPECT().Encode(gomock.Any()).Return(nil, nil).Times(2) 300 config.mockKbpki.EXPECT().FavoriteDelete(gomock.Any(), gomock.Any()). 301 Return(nil) 302 303 username := "bob" 304 tlfName := "alice,bob" 305 tlfType := tlf.Private 306 307 // Add a favorite. 308 err := f.Add(ctx, favorites.ToAdd{ 309 Folder: favorites.Folder{ 310 Name: tlfName, 311 Type: tlfType, 312 }, 313 Created: true}) 314 require.NoError(t, err) 315 316 // Put a thing in user history. 317 history := kbfsedits.NewTlfHistory() 318 _, err = history.AddNotifications(username, []string{"hello"}) 319 require.NoError(t, err) 320 config.UserHistory().UpdateHistory(tlf.CanonicalName(tlfName), tlfType, 321 history, username) 322 323 // Delete the favorite. 324 err = f.Delete(ctx, favorites.Folder{Name: tlfName, Type: tlfType}) 325 require.NoError(t, err) 326 327 // Verify that the user history is now empty. 328 userHistory := config.UserHistory().Get(username) 329 require.Empty(t, userHistory) 330 } 331 332 func TestFavoritesGetAll(t *testing.T) { 333 mockCtrl, config, ctx := favTestInit(t, false) 334 f := NewFavorites(config) 335 f.Initialize(ctx) 336 defer favTestShutdown(t, mockCtrl, config, f) 337 338 // Prep 339 teamID := keybase1.MakeTestTeamID(0xdeadbeef, false) 340 fol := keybase1.Folder{ 341 Name: "fake folder", 342 Private: true, 343 Created: false, 344 FolderType: keybase1.FolderType_TEAM, 345 TeamID: &teamID, 346 } 347 teamID2 := keybase1.MakeTestTeamID(0xabcdef, true) 348 fol2 := keybase1.Folder{ 349 Name: "another folder", 350 Private: false, 351 Created: false, 352 FolderType: keybase1.FolderType_PUBLIC, 353 TeamID: &teamID2, 354 } 355 teamID3 := keybase1.MakeTestTeamID(0x87654321, false) 356 fol3 := keybase1.Folder{ 357 Name: "folder three", 358 Private: true, 359 Created: false, 360 FolderType: keybase1.FolderType_PRIVATE, 361 TeamID: &teamID3, 362 } 363 res := keybase1.FavoritesResult{ 364 IgnoredFolders: []keybase1.Folder{fol}, 365 FavoriteFolders: []keybase1.Folder{fol2}, 366 NewFolders: []keybase1.Folder{fol3}, 367 } 368 // Mock out the API server. 369 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(res, nil) 370 config.mockCodec.EXPECT().Encode(gomock.Any()).Return(nil, nil).Times(2) 371 config.mockKbs.EXPECT().EncryptFavorites(gomock.Any(), 372 gomock.Any()).Return(nil, nil) 373 374 // Require that we correctly return the data inserted into the cache by 375 // the server. 376 res2, err := f.GetAll(ctx) 377 require.NoError(t, err) 378 require.Equal(t, res.IgnoredFolders, res2.IgnoredFolders) 379 require.Equal(t, res.NewFolders, res2.NewFolders) 380 381 // Favorites will have 2 extra: the home TLFs 382 require.Len(t, res2.FavoriteFolders, len(res.FavoriteFolders)+2) 383 for _, folder := range res.FavoriteFolders { 384 found := false 385 for _, fav := range res2.FavoriteFolders { 386 if reflect.DeepEqual(fav, folder) { 387 found = true 388 } 389 } 390 require.True(t, found, "Folder: %v", folder) 391 } 392 } 393 394 func TestFavoritesDiskCache(t *testing.T) { 395 mockCtrl, config, ctx := favTestInit(t, true) 396 397 // EXPECT this manually so that we can manually edit the leveldb later 398 config.mockKbpki.EXPECT().GetCurrentSession(gomock. 399 Any()).Times(3). 400 Return(idutil.SessionInfo{ 401 Name: kbname.NormalizedUsername("tester"), 402 UID: keybase1.MakeTestUID(16), 403 }, nil) 404 405 f := NewFavorites(config) 406 407 f.Initialize(ctx) 408 409 // Add a favorite. Expect that it will be encoded to disk. 410 fav1 := favorites.Folder{Name: "test", Type: tlf.Public} 411 fav1Add := favorites.ToAdd{ 412 Folder: fav1, 413 Data: favorites.Data{}, 414 Created: false, 415 } 416 417 var decodedData favoritesCacheForDisk 418 var decodedDataFromDisk favoritesCacheEncryptedForDisk 419 encodedData := []byte("encoded data") 420 encryptedData := []byte("encrypted data") 421 diskData := []byte("disk data") 422 config.mockKbpki.EXPECT().FavoriteAdd(gomock.Any(), gomock.Any()).Return(nil) 423 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1.FavoritesResult{ 424 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav1Add)}, 425 }, nil) 426 config.mockCodec.EXPECT().Encode(gomock.Any()).Do(func( 427 f favoritesCacheForDisk) { 428 decodedData = f 429 }).Return( 430 encodedData, 431 nil) 432 config.mockKbs.EXPECT().EncryptFavorites(gomock.Any(), 433 encodedData).Return(encryptedData, nil) 434 config.mockCodec.EXPECT().Encode(gomock.Any()).Do(func( 435 f favoritesCacheEncryptedForDisk) { 436 decodedDataFromDisk = f 437 }).Return( 438 diskData, 439 nil) 440 441 err := f.Add(ctx, fav1Add) 442 require.NoError(t, err) 443 444 key := []byte(string(keybase1.MakeTestUID(16))) 445 dataInLeveldb, err := f.diskCache.Get(key, nil) 446 require.NoError(t, err) 447 448 // Shut down the favorites cache to remove the favorites from memory. 449 err = f.Shutdown() 450 require.NoError(t, err) 451 452 // EXPECT this manually so that we can manually edit the leveldb 453 config.mockKbpki.EXPECT().GetCurrentSession(gomock. 454 Any()).Times(1). 455 Return(idutil.SessionInfo{ 456 Name: kbname.NormalizedUsername("tester"), 457 UID: keybase1.MakeTestUID(16), 458 }, nil).Do(func(_ context.Context) { 459 err := f.diskCache.Put(key, dataInLeveldb, nil) 460 require.NoError(t, err) 461 }) 462 463 // Make a new favorites cache. 464 f = NewFavorites(config) 465 466 // Initialize it from the data we just wrote to disk. 467 config.mockCodec.EXPECT().Decode(diskData, 468 gomock.Any()).Do(func(_ []byte, disk *favoritesCacheEncryptedForDisk) { 469 *disk = decodedDataFromDisk 470 }).Return(nil) 471 config.mockKbs.EXPECT().DecryptFavorites(gomock.Any(), 472 encryptedData).Return(encodedData, nil) 473 config.mockCodec.EXPECT().Decode(encodedData, 474 gomock.Any()).Do(func(_ []byte, disk *favoritesCacheForDisk) { 475 *disk = decodedData 476 }).Return(nil) 477 478 // Pretend we are offline and cannot retrieve favorites right now. 479 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()).Return(keybase1. 480 FavoritesResult{}, errDisconnected{}) 481 f.Initialize(ctx) 482 483 // Ensure that the favorite we added before is still present. 484 // There should be three favorites total, including the home TLFs. 485 faves, err := f.Get(ctx) 486 require.NoError(t, err) 487 require.Equal(t, len(faves), 3) 488 require.Contains(t, faves, fav1) 489 490 // This line not deferred because we need to swap out Favorites instances 491 // above. 492 favTestShutdown(t, mockCtrl, config, f) 493 } 494 495 func TestFavoritesRefreshCacheWhenMtimeChanged(t *testing.T) { 496 mockCtrl, config, ctx := favTestInit(t, false) 497 f := NewFavorites(config) 498 f.bufferedInterval = 1 * time.Millisecond 499 f.InitForTest() 500 defer favTestShutdown(t, mockCtrl, config, f) 501 502 fav1 := favorites.ToAdd{ 503 Folder: favorites.Folder{ 504 Name: "test", 505 Type: tlf.Public, 506 }, 507 Data: favorites.Data{}, 508 Created: false, 509 } 510 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()). 511 Return(keybase1.FavoritesResult{ 512 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav1)}, 513 }, nil) 514 515 t.Log("Refresh first time") 516 id := tlf.FakeID(1, fav1.Folder.Type) 517 f.RefreshCacheWhenMTimeChanged(ctx, id) 518 err := f.bufferedWg.Wait(ctx) 519 require.NoError(t, err) 520 521 t.Log("Refresh second time, no API call") 522 f.RefreshCacheWhenMTimeChanged(ctx, id) 523 err = f.bufferedWg.Wait(ctx) 524 require.NoError(t, err) 525 526 t.Log("A regular refresh should clear the state") 527 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()). 528 Return(keybase1.FavoritesResult{ 529 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav1)}, 530 }, nil) 531 f.RefreshCache(ctx, FavoritesRefreshModeBlocking) 532 err = f.wg.Wait(ctx) 533 require.NoError(t, err) 534 535 t.Log("Refresh third time, which should cause an API call again") 536 config.mockKbpki.EXPECT().FavoriteList(gomock.Any()). 537 Return(keybase1.FavoritesResult{ 538 FavoriteFolders: []keybase1.Folder{favToAddToKBFolder(fav1)}, 539 }, nil) 540 f.RefreshCacheWhenMTimeChanged(ctx, id) 541 err = f.bufferedWg.Wait(ctx) 542 require.NoError(t, err) 543 }