github.com/decred/dcrlnd@v0.7.6/walletunlocker/service_test.go (about) 1 package walletunlocker_test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path" 10 "testing" 11 "time" 12 13 "decred.org/dcrwallet/v4/wallet" 14 "github.com/decred/dcrd/chaincfg/v3" 15 "github.com/decred/dcrlnd/aezeed" 16 "github.com/decred/dcrlnd/keychain" 17 "github.com/decred/dcrlnd/kvdb" 18 "github.com/decred/dcrlnd/lnrpc" 19 "github.com/decred/dcrlnd/lnwallet" 20 "github.com/decred/dcrlnd/lnwallet/dcrwallet" 21 walletloader "github.com/decred/dcrlnd/lnwallet/dcrwallet/loader" 22 "github.com/decred/dcrlnd/macaroons" 23 "github.com/decred/dcrlnd/walletunlocker" 24 "github.com/stretchr/testify/require" 25 ) 26 27 var ( 28 testPassword = []byte("test-password") 29 testSeed = []byte("test-seed-123456789") 30 testMac = []byte("fakemacaroon") 31 32 testEntropy = [aezeed.EntropySize]byte{ 33 0x81, 0xb6, 0x37, 0xd8, 34 0x63, 0x59, 0xe6, 0x96, 35 0x0d, 0xe7, 0x95, 0xe4, 36 0x1e, 0x0b, 0x4c, 0xfd, 37 } 38 39 testNetParams = chaincfg.SimNetParams() 40 41 testRecoveryWindow uint32 = 150 42 43 defaultTestTimeout = 30 * time.Second 44 45 defaultRootKeyIDContext = macaroons.ContextWithRootKeyID( 46 context.Background(), macaroons.DefaultRootKeyID, 47 ) 48 ) 49 50 func createTestWallet(t *testing.T, dir string, netParams *chaincfg.Params) { 51 createTestWalletWithPw(t, testPassword, testPassword, dir, netParams) 52 } 53 54 func createTestWalletWithPw(t *testing.T, pubPw, privPw []byte, dir string, 55 netParams *chaincfg.Params) { 56 57 /* 58 // Instruct waddrmgr to use the cranked down scrypt parameters when 59 // creating new wallet encryption keys. 60 fastScrypt := waddrmgr.FastScryptOptions 61 keyGen := func(passphrase *[]byte, config *waddrmgr.ScryptOptions) ( 62 *snacl.SecretKey, error) { 63 64 return snacl.NewSecretKey( 65 passphrase, fastScrypt.N, fastScrypt.R, fastScrypt.P, 66 ) 67 } 68 waddrmgr.SetSecretKeyGen(keyGen) 69 */ 70 71 // Create a new test wallet that uses fast scrypt as KDF. 72 birthday := time.Now().Add(-time.Hour * 24) 73 netDir := dcrwallet.NetworkDir(dir, netParams) 74 loader := walletloader.NewLoader(netParams, netDir, wallet.DefaultGapLimit) 75 _, err := loader.CreateNewWallet( 76 context.Background(), pubPw, privPw, testSeed, birthday, 77 ) 78 require.NoError(t, err) 79 err = loader.UnloadWallet() 80 require.NoError(t, err) 81 } 82 83 func createSeedAndMnemonic(t *testing.T, 84 pass []byte) (*aezeed.CipherSeed, aezeed.Mnemonic) { 85 cipherSeed, err := aezeed.New( 86 keychain.KeyDerivationVersion, &testEntropy, time.Now(), 87 ) 88 require.NoError(t, err) 89 90 // With the new seed created, we'll convert it into a mnemonic phrase 91 // that we'll send over to initialize the wallet. 92 mnemonic, err := cipherSeed.ToMnemonic(pass) 93 require.NoError(t, err) 94 return cipherSeed, mnemonic 95 } 96 97 // openOrCreateTestMacStore opens or creates a bbolt DB and then initializes a 98 // root key storage for that DB and then unlocks it, creating a root key in the 99 // process. 100 func openOrCreateTestMacStore(tempDir string, pw *[]byte, 101 netParams *chaincfg.Params) (*macaroons.RootKeyStorage, error) { 102 103 netDir := dcrwallet.NetworkDir(tempDir, netParams) 104 err := os.MkdirAll(netDir, 0700) 105 if err != nil { 106 return nil, err 107 } 108 db, err := kvdb.Create( 109 kvdb.BoltBackendName, path.Join(netDir, "macaroons.db"), 110 true, kvdb.DefaultDBTimeout, 111 ) 112 if err != nil { 113 return nil, err 114 } 115 116 store, err := macaroons.NewRootKeyStorage(db) 117 if err != nil { 118 _ = db.Close() 119 return nil, err 120 } 121 122 err = store.CreateUnlock(pw) 123 if err != nil { 124 _ = store.Close() 125 return nil, err 126 } 127 _, _, err = store.RootKey(defaultRootKeyIDContext) 128 if err != nil { 129 _ = store.Close() 130 return nil, err 131 } 132 133 return store, nil 134 } 135 136 // TestGenSeedUserEntropy tests that the gen seed method generates a valid 137 // cipher seed mnemonic phrase and user provided source of entropy. 138 func TestGenSeed(t *testing.T) { 139 t.Parallel() 140 141 // First, we'll create a new test directory and unlocker service for 142 // that directory. 143 testDir, err := ioutil.TempDir("", "testcreate") 144 require.NoError(t, err) 145 defer func() { 146 _ = os.RemoveAll(testDir) 147 }() 148 149 service := walletunlocker.New( 150 testDir, testNetParams, nil, kvdb.DefaultDBTimeout, 151 "", "", "", "", 0, 152 ) 153 154 // Now that the service has been created, we'll ask it to generate a 155 // new seed for us given a test passphrase. 156 aezeedPass := []byte("kek") 157 genSeedReq := &lnrpc.GenSeedRequest{ 158 AezeedPassphrase: aezeedPass, 159 SeedEntropy: testEntropy[:], 160 } 161 162 ctx := context.Background() 163 seedResp, err := service.GenSeed(ctx, genSeedReq) 164 require.NoError(t, err) 165 166 // We should then be able to take the generated mnemonic, and properly 167 // decipher both it. 168 var mnemonic aezeed.Mnemonic 169 copy(mnemonic[:], seedResp.CipherSeedMnemonic) 170 _, err = mnemonic.ToCipherSeed(aezeedPass) 171 require.NoError(t, err) 172 } 173 174 // TestGenSeedInvalidEntropy tests that the gen seed method generates a valid 175 // cipher seed mnemonic pass phrase even when the user doesn't provide its own 176 // source of entropy. 177 func TestGenSeedGenerateEntropy(t *testing.T) { 178 t.Parallel() 179 180 // First, we'll create a new test directory and unlocker service for 181 // that directory. 182 testDir, err := ioutil.TempDir("", "testcreate") 183 require.NoError(t, err) 184 defer func() { 185 _ = os.RemoveAll(testDir) 186 }() 187 service := walletunlocker.New( 188 testDir, testNetParams, nil, kvdb.DefaultDBTimeout, 189 "", "", "", "", 0, 190 ) 191 192 // Now that the service has been created, we'll ask it to generate a 193 // new seed for us given a test passphrase. Note that we don't actually 194 aezeedPass := []byte("kek") 195 genSeedReq := &lnrpc.GenSeedRequest{ 196 AezeedPassphrase: aezeedPass, 197 } 198 199 ctx := context.Background() 200 seedResp, err := service.GenSeed(ctx, genSeedReq) 201 require.NoError(t, err) 202 203 // We should then be able to take the generated mnemonic, and properly 204 // decipher both it. 205 var mnemonic aezeed.Mnemonic 206 copy(mnemonic[:], seedResp.CipherSeedMnemonic) 207 _, err = mnemonic.ToCipherSeed(aezeedPass) 208 require.NoError(t, err) 209 } 210 211 // TestGenSeedInvalidEntropy tests that if a user attempt to create a seed with 212 // the wrong number of bytes for the initial entropy, then the proper error is 213 // returned. 214 func TestGenSeedInvalidEntropy(t *testing.T) { 215 t.Parallel() 216 217 // First, we'll create a new test directory and unlocker service for 218 // that directory. 219 testDir, err := ioutil.TempDir("", "testcreate") 220 require.NoError(t, err) 221 defer func() { 222 _ = os.RemoveAll(testDir) 223 }() 224 service := walletunlocker.New( 225 testDir, testNetParams, nil, kvdb.DefaultDBTimeout, 226 "", "", "", "", 0, 227 ) 228 229 // Now that the service has been created, we'll ask it to generate a 230 // new seed for us given a test passphrase. However, we'll be using an 231 // invalid set of entropy that's 55 bytes, instead of 15 bytes. 232 aezeedPass := []byte("kek") 233 genSeedReq := &lnrpc.GenSeedRequest{ 234 AezeedPassphrase: aezeedPass, 235 SeedEntropy: bytes.Repeat([]byte("a"), 55), 236 } 237 238 // We should get an error now since the entropy source was invalid. 239 ctx := context.Background() 240 _, err = service.GenSeed(ctx, genSeedReq) 241 require.Error(t, err) 242 require.Contains(t, err.Error(), "incorrect entropy length") 243 } 244 245 // TestInitWallet tests that the user is able to properly initialize the wallet 246 // given an existing cipher seed passphrase. 247 func TestInitWallet(t *testing.T) { 248 t.Parallel() 249 250 // testDir is empty, meaning wallet was not created from before. 251 testDir, err := ioutil.TempDir("", "testcreate") 252 require.NoError(t, err) 253 defer func() { 254 _ = os.RemoveAll(testDir) 255 }() 256 257 // Create new UnlockerService. 258 service := walletunlocker.New( 259 testDir, testNetParams, nil, kvdb.DefaultDBTimeout, 260 "", "", "", "", 0, 261 ) 262 263 // Once we have the unlocker service created, we'll now instantiate a 264 // new cipher seed and its mnemonic. 265 pass := []byte("test") 266 cipherSeed, mnemonic := createSeedAndMnemonic(t, pass) 267 268 // Now that we have all the necessary items, we'll now issue the Init 269 // command to the wallet. This should check the validity of the cipher 270 // seed, then send over the initialization information over the init 271 // channel. 272 ctx := context.Background() 273 req := &lnrpc.InitWalletRequest{ 274 WalletPassword: testPassword, 275 CipherSeedMnemonic: mnemonic[:], 276 AezeedPassphrase: pass, 277 RecoveryWindow: int32(testRecoveryWindow), 278 StatelessInit: true, 279 } 280 errChan := make(chan error, 1) 281 go func() { 282 response, err := service.InitWallet(ctx, req) 283 if err != nil { 284 errChan <- err 285 return 286 } 287 288 if !bytes.Equal(response.AdminMacaroon, testMac) { 289 errChan <- fmt.Errorf("mismatched macaroon: "+ 290 "expected %x, got %x", testMac, 291 response.AdminMacaroon) 292 } 293 }() 294 295 // The same user passphrase, and also the plaintext cipher seed 296 // should be sent over and match exactly. 297 select { 298 case err := <-errChan: 299 t.Fatalf("InitWallet call failed: %v", err) 300 301 case msg := <-service.InitMsgs: 302 msgSeed := msg.WalletSeed 303 require.Equal(t, testPassword, msg.Passphrase) 304 require.Equal( 305 t, cipherSeed.InternalVersion, msgSeed.InternalVersion, 306 ) 307 require.Equal(t, cipherSeed.Birthday, msgSeed.Birthday) 308 require.Equal(t, cipherSeed.Entropy, msgSeed.Entropy) 309 require.Equal(t, testRecoveryWindow, msg.RecoveryWindow) 310 require.Equal(t, true, msg.StatelessInit) 311 312 // Send a fake macaroon that should be returned in the response 313 // in the async code above. 314 service.MacResponseChan <- testMac 315 316 case <-time.After(defaultTestTimeout): 317 t.Fatalf("password not received") 318 } 319 320 // Create a wallet in testDir. 321 createTestWallet(t, testDir, testNetParams) 322 323 // Now calling InitWallet should fail, since a wallet already exists in 324 // the directory. 325 _, err = service.InitWallet(ctx, req) 326 require.Error(t, err) 327 328 // Similarly, if we try to do GenSeed again, we should get an error as 329 // the wallet already exists. 330 _, err = service.GenSeed(ctx, &lnrpc.GenSeedRequest{}) 331 require.Error(t, err) 332 } 333 334 // TestInitWalletInvalidCipherSeed tests that if we attempt to create a wallet 335 // with an invalid cipher seed, then we'll receive an error. 336 func TestCreateWalletInvalidEntropy(t *testing.T) { 337 t.Parallel() 338 339 // testDir is empty, meaning wallet was not created from before. 340 testDir, err := ioutil.TempDir("", "testcreate") 341 require.NoError(t, err) 342 defer func() { 343 _ = os.RemoveAll(testDir) 344 }() 345 346 // Create new UnlockerService. 347 service := walletunlocker.New( 348 testDir, testNetParams, nil, kvdb.DefaultDBTimeout, 349 "", "", "", "", 0, 350 ) 351 352 // We'll attempt to init the wallet with an invalid cipher seed and 353 // passphrase. 354 req := &lnrpc.InitWalletRequest{ 355 WalletPassword: testPassword, 356 CipherSeedMnemonic: []string{"invalid", "seed"}, 357 AezeedPassphrase: []byte("fake pass"), 358 } 359 360 ctx := context.Background() 361 _, err = service.InitWallet(ctx, req) 362 require.Error(t, err) 363 } 364 365 // TestUnlockWallet checks that trying to unlock non-existing wallet fail, that 366 // unlocking existing wallet with wrong passphrase fails, and that unlocking 367 // existing wallet with correct passphrase succeeds. 368 func TestUnlockWallet(t *testing.T) { 369 t.Parallel() 370 371 // testDir is empty, meaning wallet was not created from before. 372 testDir, err := ioutil.TempDir("", "testunlock") 373 require.NoError(t, err) 374 defer func() { 375 _ = os.RemoveAll(testDir) 376 }() 377 378 // Create new UnlockerService that'll also drop the wallet's history on 379 // unlock. 380 service := walletunlocker.New( 381 testDir, testNetParams, nil, kvdb.DefaultDBTimeout, 382 "", "", "", "", 0, 383 ) 384 385 ctx := context.Background() 386 req := &lnrpc.UnlockWalletRequest{ 387 WalletPassword: testPassword, 388 RecoveryWindow: int32(testRecoveryWindow), 389 StatelessInit: true, 390 } 391 392 // Should fail to unlock non-existing wallet. 393 _, err = service.UnlockWallet(ctx, req) 394 require.Error(t, err) 395 396 // Create a wallet we can try to unlock. 397 createTestWallet(t, testDir, testNetParams) 398 399 // Try unlocking this wallet with the wrong passphrase. 400 wrongReq := &lnrpc.UnlockWalletRequest{ 401 WalletPassword: []byte("wrong-ofc"), 402 } 403 _, err = service.UnlockWallet(ctx, wrongReq) 404 require.Error(t, err) 405 406 // With the correct password, we should be able to unlock the wallet. 407 errChan := make(chan error, 1) 408 go func() { 409 // With the correct password, we should be able to unlock the 410 // wallet. 411 _, err := service.UnlockWallet(ctx, req) 412 if err != nil { 413 errChan <- err 414 } 415 }() 416 417 // Password and recovery window should be sent over the channel. 418 select { 419 case err := <-errChan: 420 t.Fatalf("UnlockWallet call failed: %v", err) 421 422 case unlockMsg := <-service.UnlockMsgs: 423 require.Equal(t, testPassword, unlockMsg.Passphrase) 424 require.Equal(t, testRecoveryWindow, unlockMsg.RecoveryWindow) 425 require.Equal(t, true, unlockMsg.StatelessInit) 426 427 // Send a fake macaroon that should be returned in the response 428 // in the async code above. 429 service.MacResponseChan <- testMac 430 431 case <-time.After(defaultTestTimeout): 432 t.Fatalf("password not received") 433 } 434 } 435 436 // TestChangeWalletPasswordNewRootkey tests that we can successfully change the 437 // wallet's password needed to unlock it and rotate the root key for the 438 // macaroons in the same process. 439 func TestChangeWalletPasswordNewRootkey(t *testing.T) { 440 t.Parallel() 441 442 // testDir is empty, meaning wallet was not created from before. 443 testDir, err := ioutil.TempDir("", "testchangepassword") 444 require.NoError(t, err) 445 defer func() { 446 _ = os.RemoveAll(testDir) 447 }() 448 449 // Changing the password of the wallet will also try to change the 450 // password of the macaroon DB. We create a default DB here but close it 451 // immediately so the service does not fail when trying to open it. 452 store, err := openOrCreateTestMacStore( 453 testDir, &testPassword, testNetParams, 454 ) 455 require.NoError(t, err) 456 require.NoError(t, store.Close()) 457 458 // Create some files that will act as macaroon files that should be 459 // deleted after a password change is successful with a new root key 460 // requested. 461 var tempFiles []string 462 for i := 0; i < 3; i++ { 463 file, err := ioutil.TempFile(testDir, "") 464 if err != nil { 465 t.Fatalf("unable to create temp file: %v", err) 466 } 467 tempFiles = append(tempFiles, file.Name()) 468 require.NoError(t, file.Close()) 469 } 470 471 // Create a new UnlockerService with our temp files. 472 service := walletunlocker.New( 473 testDir, testNetParams, tempFiles, kvdb.DefaultDBTimeout, 474 "", "", "", "", 0, 475 ) 476 service.SetMacaroonDB(store.Backend) 477 478 ctx := context.Background() 479 newPassword := []byte("hunter2???") 480 481 req := &lnrpc.ChangePasswordRequest{ 482 CurrentPassword: testPassword, 483 NewPassword: newPassword, 484 NewMacaroonRootKey: true, 485 } 486 487 // Changing the password to a non-existing wallet should fail. 488 _, err = service.ChangePassword(ctx, req) 489 require.Error(t, err) 490 491 // Create a wallet to test changing the password. 492 createTestWallet(t, testDir, testNetParams) 493 494 // Attempting to change the wallet's password using an incorrect 495 // current password should fail. 496 wrongReq := &lnrpc.ChangePasswordRequest{ 497 CurrentPassword: []byte("wrong-ofc"), 498 NewPassword: newPassword, 499 } 500 _, err = service.ChangePassword(ctx, wrongReq) 501 require.Error(t, err) 502 503 // The files should still exist after an unsuccessful attempt to change 504 // the wallet's password. 505 for _, tempFile := range tempFiles { 506 if _, err := os.Stat(tempFile); os.IsNotExist(err) { 507 t.Fatal("file does not exist but it should") 508 } 509 } 510 511 // Attempting to change the wallet's password using an invalid 512 // new password should fail. 513 wrongReq.NewPassword = []byte("8") 514 _, err = service.ChangePassword(ctx, wrongReq) 515 require.Error(t, err) 516 517 // When providing the correct wallet's current password and a new 518 // password that meets the length requirement, the password change 519 // should succeed. 520 errChan := make(chan error, 1) 521 go doChangePassword(service, testDir, req, errChan) 522 523 // The new password should be sent over the channel. 524 select { 525 case err := <-errChan: 526 t.Fatalf("ChangePassword call failed: %v", err) 527 528 case unlockMsg := <-service.UnlockMsgs: 529 require.Equal(t, newPassword, unlockMsg.Passphrase) 530 531 // Send a fake macaroon that should be returned in the response 532 // in the async code above. 533 service.MacResponseChan <- testMac 534 535 case <-time.After(defaultTestTimeout): 536 t.Fatalf("password not received") 537 } 538 539 // The files should no longer exist. 540 for _, tempFile := range tempFiles { 541 if _, err := os.Open(tempFile); err == nil { 542 t.Fatal("file exists but it shouldn't") 543 } 544 } 545 } 546 547 // TestChangeWalletPasswordStateless checks that trying to change the password 548 // of an existing wallet that was initialized stateless works when when the 549 // --stateless_init flat is set. Also checks that if no password is given, 550 // the default password is used. 551 func TestChangeWalletPasswordStateless(t *testing.T) { 552 t.Parallel() 553 554 // testDir is empty, meaning wallet was not created from before. 555 testDir, err := ioutil.TempDir("", "testchangepasswordstateless") 556 require.NoError(t, err) 557 defer func() { 558 _ = os.RemoveAll(testDir) 559 }() 560 561 // Changing the password of the wallet will also try to change the 562 // password of the macaroon DB. We create a default DB here but close it 563 // immediately so the service does not fail when trying to open it. 564 store, err := openOrCreateTestMacStore( 565 testDir, &lnwallet.DefaultPrivatePassphrase, testNetParams, 566 ) 567 require.NoError(t, err) 568 require.NoError(t, store.Close()) 569 570 // Create a temp file that will act as the macaroon DB file that will 571 // be deleted by changing the password. 572 tmpFile, err := ioutil.TempFile(testDir, "") 573 require.NoError(t, err) 574 tempMacFile := tmpFile.Name() 575 err = tmpFile.Close() 576 require.NoError(t, err) 577 578 // Create a file name that does not exist that will be used as a 579 // macaroon file reference. The fact that the file does not exist should 580 // not throw an error when --stateless_init is used. 581 nonExistingFile := path.Join(testDir, "does-not-exist") 582 583 // Create a new UnlockerService with our temp files. 584 service := walletunlocker.New( 585 testDir, testNetParams, []string{ 586 tempMacFile, nonExistingFile, 587 }, kvdb.DefaultDBTimeout, "", "", "", "", 0, 588 ) 589 service.SetMacaroonDB(store.Backend) 590 591 // Create a wallet we can try to unlock. We use the default password 592 // so we can check that the unlocker service defaults to this when 593 // we give it an empty CurrentPassword to indicate we come from a 594 // --noencryptwallet state. 595 createTestWalletWithPw( 596 t, lnwallet.DefaultPublicPassphrase, 597 lnwallet.DefaultPrivatePassphrase, testDir, testNetParams, 598 ) 599 600 // We make sure that we get a proper error message if we forget to 601 // add the --stateless_init flag but the macaroon files don't exist. 602 badReq := &lnrpc.ChangePasswordRequest{ 603 NewPassword: testPassword, 604 NewMacaroonRootKey: true, 605 } 606 ctx := context.Background() 607 _, err = service.ChangePassword(ctx, badReq) 608 require.Error(t, err) 609 610 // Prepare the correct request we are going to send to the unlocker 611 // service. We don't provide a current password to indicate there 612 // was none set before. 613 req := &lnrpc.ChangePasswordRequest{ 614 NewPassword: testPassword, 615 StatelessInit: true, 616 NewMacaroonRootKey: true, 617 } 618 619 // Since we indicated the wallet was initialized stateless, the service 620 // will block until it receives the macaroon through the channel 621 // provided in the message in UnlockMsgs. So we need to call the service 622 // async and then wait for the unlock message to arrive so we can send 623 // back a fake macaroon. 624 errChan := make(chan error, 1) 625 go doChangePassword(service, testDir, req, errChan) 626 627 // Password and recovery window should be sent over the channel. 628 select { 629 case err := <-errChan: 630 t.Fatalf("ChangePassword call failed: %v", err) 631 632 case unlockMsg := <-service.UnlockMsgs: 633 require.Equal(t, testPassword, unlockMsg.Passphrase) 634 635 // Send a fake macaroon that should be returned in the response 636 // in the async code above. 637 service.MacResponseChan <- testMac 638 639 case <-time.After(defaultTestTimeout): 640 t.Fatalf("password not received") 641 } 642 } 643 644 func doChangePassword(service *walletunlocker.UnlockerService, testDir string, 645 req *lnrpc.ChangePasswordRequest, errChan chan error) { 646 647 // When providing the correct wallet's current password and a 648 // new password that meets the length requirement, the password 649 // change should succeed. 650 ctx := context.Background() 651 response, err := service.ChangePassword(ctx, req) 652 if err != nil { 653 errChan <- fmt.Errorf("could not change password: %v", err) 654 return 655 } 656 657 if !bytes.Equal(response.AdminMacaroon, testMac) { 658 errChan <- fmt.Errorf("mismatched macaroon: expected "+ 659 "%x, got %x", testMac, response.AdminMacaroon) 660 } 661 662 // Close the macaroon DB and try to open it and read the root 663 // key with the new password. 664 store, err := openOrCreateTestMacStore( 665 testDir, &testPassword, testNetParams, 666 ) 667 if err != nil { 668 errChan <- fmt.Errorf("could not create test store: %v", err) 669 return 670 } 671 _, _, err = store.RootKey(defaultRootKeyIDContext) 672 if err != nil { 673 errChan <- fmt.Errorf("could not get root key: %v", err) 674 return 675 } 676 677 // Do cleanup now. Since we are in a go func, the defer at the 678 // top of the outer would not work, because it would delete 679 // the directory before we could check the content in here. 680 err = store.Close() 681 if err != nil { 682 errChan <- fmt.Errorf("could not close store: %v", err) 683 return 684 } 685 err = os.RemoveAll(testDir) 686 if err != nil { 687 errChan <- err 688 return 689 } 690 }