8d066f1f42
This commit replaces ioutil.TempDir with t.TempDir in tests. The directory created by t.TempDir is automatically removed when the test and all its subtests complete. Prior to this commit, temporary directory created using ioutil.TempDir had to be removed manually by calling os.RemoveAll, which is omitted in some tests. The error handling boilerplate e.g. defer func() { if err := os.RemoveAll(dir); err != nil { t.Fatal(err) } } is also tedious, but t.TempDir handles this for us nicely. Reference: https://pkg.go.dev/testing#T.TempDir Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
406 lines
13 KiB
Go
406 lines
13 KiB
Go
// Copyright 2017 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package keystore
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cespare/cp"
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/ethereum/go-ethereum/accounts"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
)
|
|
|
|
var (
|
|
cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore"))
|
|
cachetestAccounts = []accounts.Account{
|
|
{
|
|
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")},
|
|
},
|
|
}
|
|
)
|
|
|
|
func TestWatchNewFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir, ks := tmpKeyStore(t, false)
|
|
|
|
// Ensure the watcher is started before adding any files.
|
|
ks.Accounts()
|
|
time.Sleep(1000 * time.Millisecond)
|
|
|
|
// Move in the files.
|
|
wantAccounts := make([]accounts.Account, len(cachetestAccounts))
|
|
for i := range cachetestAccounts {
|
|
wantAccounts[i] = accounts.Account{
|
|
Address: cachetestAccounts[i].Address,
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))},
|
|
}
|
|
if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// ks should see the accounts.
|
|
var list []accounts.Account
|
|
for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 {
|
|
list = ks.Accounts()
|
|
if reflect.DeepEqual(list, wantAccounts) {
|
|
// ks should have also received change notifications
|
|
select {
|
|
case <-ks.changes:
|
|
default:
|
|
t.Fatalf("wasn't notified of new accounts")
|
|
}
|
|
return
|
|
}
|
|
time.Sleep(d)
|
|
}
|
|
t.Errorf("got %s, want %s", spew.Sdump(list), spew.Sdump(wantAccounts))
|
|
}
|
|
|
|
func TestWatchNoDir(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create ks but not the directory that it watches.
|
|
rand.Seed(time.Now().UnixNano())
|
|
dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watchnodir-test-%d-%d", os.Getpid(), rand.Int()))
|
|
ks := NewKeyStore(dir, LightScryptN, LightScryptP)
|
|
|
|
list := ks.Accounts()
|
|
if len(list) > 0 {
|
|
t.Error("initial account list not empty:", list)
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Create the directory and copy a key file into it.
|
|
os.MkdirAll(dir, 0700)
|
|
defer os.RemoveAll(dir)
|
|
file := filepath.Join(dir, "aaa")
|
|
if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// ks should see the account.
|
|
wantAccounts := []accounts.Account{cachetestAccounts[0]}
|
|
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file}
|
|
for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 {
|
|
list = ks.Accounts()
|
|
if reflect.DeepEqual(list, wantAccounts) {
|
|
// ks should have also received change notifications
|
|
select {
|
|
case <-ks.changes:
|
|
default:
|
|
t.Fatalf("wasn't notified of new accounts")
|
|
}
|
|
return
|
|
}
|
|
time.Sleep(d)
|
|
}
|
|
t.Errorf("\ngot %v\nwant %v", list, wantAccounts)
|
|
}
|
|
|
|
func TestCacheInitialReload(t *testing.T) {
|
|
cache, _ := newAccountCache(cachetestDir)
|
|
accounts := cache.accounts()
|
|
if !reflect.DeepEqual(accounts, cachetestAccounts) {
|
|
t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts))
|
|
}
|
|
}
|
|
|
|
func TestCacheAddDeleteOrder(t *testing.T) {
|
|
cache, _ := newAccountCache("testdata/no-such-dir")
|
|
cache.watcher.running = true // prevent unexpected reloads
|
|
|
|
accs := []accounts.Account{
|
|
{
|
|
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"},
|
|
},
|
|
}
|
|
for _, a := range accs {
|
|
cache.add(a)
|
|
}
|
|
// Add some of them twice to check that they don't get reinserted.
|
|
cache.add(accs[0])
|
|
cache.add(accs[2])
|
|
|
|
// Check that the account list is sorted by filename.
|
|
wantAccounts := make([]accounts.Account, len(accs))
|
|
copy(wantAccounts, accs)
|
|
sort.Sort(accountsByURL(wantAccounts))
|
|
list := cache.accounts()
|
|
if !reflect.DeepEqual(list, wantAccounts) {
|
|
t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts))
|
|
}
|
|
for _, a := range accs {
|
|
if !cache.hasAddress(a.Address) {
|
|
t.Errorf("expected hasAccount(%x) to return true", a.Address)
|
|
}
|
|
}
|
|
if cache.hasAddress(common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) {
|
|
t.Errorf("expected hasAccount(%x) to return false", common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"))
|
|
}
|
|
|
|
// Delete a few keys from the cache.
|
|
for i := 0; i < len(accs); i += 2 {
|
|
cache.delete(wantAccounts[i])
|
|
}
|
|
cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}})
|
|
|
|
// Check content again after deletion.
|
|
wantAccountsAfterDelete := []accounts.Account{
|
|
wantAccounts[1],
|
|
wantAccounts[3],
|
|
wantAccounts[5],
|
|
}
|
|
list = cache.accounts()
|
|
if !reflect.DeepEqual(list, wantAccountsAfterDelete) {
|
|
t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete))
|
|
}
|
|
for _, a := range wantAccountsAfterDelete {
|
|
if !cache.hasAddress(a.Address) {
|
|
t.Errorf("expected hasAccount(%x) to return true", a.Address)
|
|
}
|
|
}
|
|
if cache.hasAddress(wantAccounts[0].Address) {
|
|
t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address)
|
|
}
|
|
}
|
|
|
|
func TestCacheFind(t *testing.T) {
|
|
dir := filepath.Join("testdata", "dir")
|
|
cache, _ := newAccountCache(dir)
|
|
cache.watcher.running = true // prevent unexpected reloads
|
|
|
|
accs := []accounts.Account{
|
|
{
|
|
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")},
|
|
},
|
|
{
|
|
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")},
|
|
},
|
|
}
|
|
for _, a := range accs {
|
|
cache.add(a)
|
|
}
|
|
|
|
nomatchAccount := accounts.Account{
|
|
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
|
|
URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")},
|
|
}
|
|
tests := []struct {
|
|
Query accounts.Account
|
|
WantResult accounts.Account
|
|
WantError error
|
|
}{
|
|
// by address
|
|
{Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]},
|
|
// by file
|
|
{Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]},
|
|
// by basename
|
|
{Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]},
|
|
// by file and address
|
|
{Query: accs[0], WantResult: accs[0]},
|
|
// ambiguous address, tie resolved by file
|
|
{Query: accs[2], WantResult: accs[2]},
|
|
// ambiguous address error
|
|
{
|
|
Query: accounts.Account{Address: accs[2].Address},
|
|
WantError: &AmbiguousAddrError{
|
|
Addr: accs[2].Address,
|
|
Matches: []accounts.Account{accs[2], accs[3]},
|
|
},
|
|
},
|
|
// no match error
|
|
{Query: nomatchAccount, WantError: ErrNoMatch},
|
|
{Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch},
|
|
{Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: ErrNoMatch},
|
|
{Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch},
|
|
}
|
|
for i, test := range tests {
|
|
a, err := cache.find(test.Query)
|
|
if !reflect.DeepEqual(err, test.WantError) {
|
|
t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError)
|
|
continue
|
|
}
|
|
if a != test.WantResult {
|
|
t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func waitForAccounts(wantAccounts []accounts.Account, ks *KeyStore) error {
|
|
var list []accounts.Account
|
|
for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 {
|
|
list = ks.Accounts()
|
|
if reflect.DeepEqual(list, wantAccounts) {
|
|
// ks should have also received change notifications
|
|
select {
|
|
case <-ks.changes:
|
|
default:
|
|
return fmt.Errorf("wasn't notified of new accounts")
|
|
}
|
|
return nil
|
|
}
|
|
time.Sleep(d)
|
|
}
|
|
return fmt.Errorf("\ngot %v\nwant %v", list, wantAccounts)
|
|
}
|
|
|
|
// TestUpdatedKeyfileContents tests that updating the contents of a keystore file
|
|
// is noticed by the watcher, and the account cache is updated accordingly
|
|
func TestUpdatedKeyfileContents(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a temporary kesytore to test with
|
|
rand.Seed(time.Now().UnixNano())
|
|
dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-updatedkeyfilecontents-test-%d-%d", os.Getpid(), rand.Int()))
|
|
ks := NewKeyStore(dir, LightScryptN, LightScryptP)
|
|
|
|
list := ks.Accounts()
|
|
if len(list) > 0 {
|
|
t.Error("initial account list not empty:", list)
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Create the directory and copy a key file into it.
|
|
os.MkdirAll(dir, 0700)
|
|
defer os.RemoveAll(dir)
|
|
file := filepath.Join(dir, "aaa")
|
|
|
|
// Place one of our testfiles in there
|
|
if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// ks should see the account.
|
|
wantAccounts := []accounts.Account{cachetestAccounts[0]}
|
|
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file}
|
|
if err := waitForAccounts(wantAccounts, ks); err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
|
|
// needed so that modTime of `file` is different to its current value after forceCopyFile
|
|
time.Sleep(1000 * time.Millisecond)
|
|
|
|
// Now replace file contents
|
|
if err := forceCopyFile(file, cachetestAccounts[1].URL.Path); err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
wantAccounts = []accounts.Account{cachetestAccounts[1]}
|
|
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file}
|
|
if err := waitForAccounts(wantAccounts, ks); err != nil {
|
|
t.Errorf("First replacement failed")
|
|
t.Error(err)
|
|
return
|
|
}
|
|
|
|
// needed so that modTime of `file` is different to its current value after forceCopyFile
|
|
time.Sleep(1000 * time.Millisecond)
|
|
|
|
// Now replace file contents again
|
|
if err := forceCopyFile(file, cachetestAccounts[2].URL.Path); err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
wantAccounts = []accounts.Account{cachetestAccounts[2]}
|
|
wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file}
|
|
if err := waitForAccounts(wantAccounts, ks); err != nil {
|
|
t.Errorf("Second replacement failed")
|
|
t.Error(err)
|
|
return
|
|
}
|
|
|
|
// needed so that modTime of `file` is different to its current value after ioutil.WriteFile
|
|
time.Sleep(1000 * time.Millisecond)
|
|
|
|
// Now replace file contents with crap
|
|
if err := ioutil.WriteFile(file, []byte("foo"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
return
|
|
}
|
|
if err := waitForAccounts([]accounts.Account{}, ks); err != nil {
|
|
t.Errorf("Emptying account file failed")
|
|
t.Error(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// forceCopyFile is like cp.CopyFile, but doesn't complain if the destination exists.
|
|
func forceCopyFile(dst, src string) error {
|
|
data, err := ioutil.ReadFile(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutil.WriteFile(dst, data, 0644)
|
|
}
|