5346ed8a5c
This commit extends compatibility with the bbolt kvdb implementation, which returns ErrIncompatibleValue in case of a bucket/value key collision. Furthermore the commit also adds an extra precondition to the transaction when a key doesn't exist. This is needed as we fix reads to a snapshot revision and other writers may commit the key otherwise.
524 lines
13 KiB
Go
524 lines
13 KiB
Go
// +build kvdb_etcd
|
|
|
|
package etcd
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestBucketCreation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := NewEtcdTestFixture(t)
|
|
defer f.Cleanup()
|
|
|
|
db, err := newEtcdBackend(f.BackendConfig())
|
|
require.NoError(t, err)
|
|
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
// empty bucket name
|
|
b, err := tx.CreateTopLevelBucket(nil)
|
|
require.Error(t, walletdb.ErrBucketNameRequired, err)
|
|
require.Nil(t, b)
|
|
|
|
// empty bucket name
|
|
b, err = tx.CreateTopLevelBucket([]byte(""))
|
|
require.Error(t, walletdb.ErrBucketNameRequired, err)
|
|
require.Nil(t, b)
|
|
|
|
// "apple"
|
|
apple, err := tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, apple)
|
|
|
|
// Check bucket tx.
|
|
require.Equal(t, tx, apple.Tx())
|
|
|
|
// "apple" already created
|
|
b, err = tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, b)
|
|
|
|
// "apple/banana"
|
|
banana, err := apple.CreateBucket([]byte("banana"))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, banana)
|
|
|
|
banana, err = apple.CreateBucketIfNotExists([]byte("banana"))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, banana)
|
|
|
|
// Try creating "apple/banana" again
|
|
b, err = apple.CreateBucket([]byte("banana"))
|
|
require.Error(t, walletdb.ErrBucketExists, err)
|
|
require.Nil(t, b)
|
|
|
|
// "apple/mango"
|
|
mango, err := apple.CreateBucket([]byte("mango"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, mango)
|
|
|
|
// "apple/banana/pear"
|
|
pear, err := banana.CreateBucket([]byte("pear"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, pear)
|
|
|
|
// empty bucket
|
|
require.Nil(t, apple.NestedReadWriteBucket(nil))
|
|
require.Nil(t, apple.NestedReadWriteBucket([]byte("")))
|
|
|
|
// "apple/pear" doesn't exist
|
|
require.Nil(t, apple.NestedReadWriteBucket([]byte("pear")))
|
|
|
|
// "apple/banana" exits
|
|
require.NotNil(t, apple.NestedReadWriteBucket([]byte("banana")))
|
|
require.NotNil(t, apple.NestedReadBucket([]byte("banana")))
|
|
return nil
|
|
})
|
|
|
|
require.Nil(t, err)
|
|
|
|
expected := map[string]string{
|
|
bkey("apple"): bval("apple"),
|
|
bkey("apple", "banana"): bval("apple", "banana"),
|
|
bkey("apple", "mango"): bval("apple", "mango"),
|
|
bkey("apple", "banana", "pear"): bval("apple", "banana", "pear"),
|
|
}
|
|
require.Equal(t, expected, f.Dump())
|
|
}
|
|
|
|
func TestBucketDeletion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := NewEtcdTestFixture(t)
|
|
defer f.Cleanup()
|
|
|
|
db, err := newEtcdBackend(f.BackendConfig())
|
|
require.NoError(t, err)
|
|
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
// "apple"
|
|
apple, err := tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, apple)
|
|
|
|
// "apple/banana"
|
|
banana, err := apple.CreateBucket([]byte("banana"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, banana)
|
|
|
|
kvs := []KV{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}}
|
|
|
|
for _, kv := range kvs {
|
|
require.NoError(t, banana.Put([]byte(kv.key), []byte(kv.val)))
|
|
require.Equal(t, []byte(kv.val), banana.Get([]byte(kv.key)))
|
|
}
|
|
|
|
// Delete a k/v from "apple/banana"
|
|
require.NoError(t, banana.Delete([]byte("key2")))
|
|
// Try getting/putting/deleting invalid k/v's.
|
|
require.Nil(t, banana.Get(nil))
|
|
require.Error(t, walletdb.ErrKeyRequired, banana.Put(nil, []byte("val")))
|
|
require.Error(t, walletdb.ErrKeyRequired, banana.Delete(nil))
|
|
|
|
// Try deleting a k/v that doesn't exist.
|
|
require.NoError(t, banana.Delete([]byte("nokey")))
|
|
|
|
// "apple/pear"
|
|
pear, err := apple.CreateBucket([]byte("pear"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, pear)
|
|
|
|
// Put some values into "apple/pear"
|
|
for _, kv := range kvs {
|
|
require.Nil(t, pear.Put([]byte(kv.key), []byte(kv.val)))
|
|
require.Equal(t, []byte(kv.val), pear.Get([]byte(kv.key)))
|
|
}
|
|
|
|
// Create nested bucket "apple/pear/cherry"
|
|
cherry, err := pear.CreateBucket([]byte("cherry"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, cherry)
|
|
|
|
// Put some values into "apple/pear/cherry"
|
|
for _, kv := range kvs {
|
|
require.NoError(t, cherry.Put([]byte(kv.key), []byte(kv.val)))
|
|
}
|
|
|
|
// Read back values in "apple/pear/cherry" trough a read bucket.
|
|
cherryReadBucket := pear.NestedReadBucket([]byte("cherry"))
|
|
for _, kv := range kvs {
|
|
require.Equal(
|
|
t, []byte(kv.val),
|
|
cherryReadBucket.Get([]byte(kv.key)),
|
|
)
|
|
}
|
|
|
|
// Try deleting some invalid buckets.
|
|
require.Error(t,
|
|
walletdb.ErrBucketNameRequired, apple.DeleteNestedBucket(nil),
|
|
)
|
|
|
|
// Try deleting a non existing bucket.
|
|
require.Error(
|
|
t,
|
|
walletdb.ErrBucketNotFound,
|
|
apple.DeleteNestedBucket([]byte("missing")),
|
|
)
|
|
|
|
// Delete "apple/pear"
|
|
require.Nil(t, apple.DeleteNestedBucket([]byte("pear")))
|
|
|
|
// "apple/pear" deleted
|
|
require.Nil(t, apple.NestedReadWriteBucket([]byte("pear")))
|
|
|
|
// "apple/pear/cherry" deleted
|
|
require.Nil(t, pear.NestedReadWriteBucket([]byte("cherry")))
|
|
|
|
// Values deleted too.
|
|
for _, kv := range kvs {
|
|
require.Nil(t, pear.Get([]byte(kv.key)))
|
|
require.Nil(t, cherry.Get([]byte(kv.key)))
|
|
}
|
|
|
|
// "aple/banana" exists
|
|
require.NotNil(t, apple.NestedReadWriteBucket([]byte("banana")))
|
|
return nil
|
|
})
|
|
|
|
require.Nil(t, err)
|
|
|
|
expected := map[string]string{
|
|
bkey("apple"): bval("apple"),
|
|
bkey("apple", "banana"): bval("apple", "banana"),
|
|
vkey("key1", "apple", "banana"): "val1",
|
|
vkey("key3", "apple", "banana"): "val3",
|
|
}
|
|
require.Equal(t, expected, f.Dump())
|
|
}
|
|
|
|
func TestBucketForEach(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := NewEtcdTestFixture(t)
|
|
defer f.Cleanup()
|
|
|
|
db, err := newEtcdBackend(f.BackendConfig())
|
|
require.NoError(t, err)
|
|
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
// "apple"
|
|
apple, err := tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, apple)
|
|
|
|
// "apple/banana"
|
|
banana, err := apple.CreateBucket([]byte("banana"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, banana)
|
|
|
|
kvs := []KV{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}}
|
|
|
|
// put some values into "apple" and "apple/banana" too
|
|
for _, kv := range kvs {
|
|
require.Nil(t, apple.Put([]byte(kv.key), []byte(kv.val)))
|
|
require.Equal(t, []byte(kv.val), apple.Get([]byte(kv.key)))
|
|
|
|
require.Nil(t, banana.Put([]byte(kv.key), []byte(kv.val)))
|
|
require.Equal(t, []byte(kv.val), banana.Get([]byte(kv.key)))
|
|
}
|
|
|
|
got := make(map[string]string)
|
|
err = apple.ForEach(func(key, val []byte) error {
|
|
got[string(key)] = string(val)
|
|
return nil
|
|
})
|
|
|
|
expected := map[string]string{
|
|
"key1": "val1",
|
|
"key2": "val2",
|
|
"key3": "val3",
|
|
"banana": "",
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, got)
|
|
|
|
got = make(map[string]string)
|
|
err = banana.ForEach(func(key, val []byte) error {
|
|
got[string(key)] = string(val)
|
|
return nil
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
// remove the sub-bucket key
|
|
delete(expected, "banana")
|
|
require.Equal(t, expected, got)
|
|
|
|
return nil
|
|
})
|
|
|
|
require.Nil(t, err)
|
|
|
|
expected := map[string]string{
|
|
bkey("apple"): bval("apple"),
|
|
bkey("apple", "banana"): bval("apple", "banana"),
|
|
vkey("key1", "apple"): "val1",
|
|
vkey("key2", "apple"): "val2",
|
|
vkey("key3", "apple"): "val3",
|
|
vkey("key1", "apple", "banana"): "val1",
|
|
vkey("key2", "apple", "banana"): "val2",
|
|
vkey("key3", "apple", "banana"): "val3",
|
|
}
|
|
require.Equal(t, expected, f.Dump())
|
|
}
|
|
|
|
func TestBucketForEachWithError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := NewEtcdTestFixture(t)
|
|
defer f.Cleanup()
|
|
|
|
db, err := newEtcdBackend(f.BackendConfig())
|
|
require.NoError(t, err)
|
|
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
// "apple"
|
|
apple, err := tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, apple)
|
|
|
|
// "apple/banana"
|
|
banana, err := apple.CreateBucket([]byte("banana"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, banana)
|
|
|
|
// "apple/pear"
|
|
pear, err := apple.CreateBucket([]byte("pear"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, pear)
|
|
|
|
kvs := []KV{{"key1", "val1"}, {"key2", "val2"}}
|
|
|
|
// Put some values into "apple" and "apple/banana" too.
|
|
for _, kv := range kvs {
|
|
require.Nil(t, apple.Put([]byte(kv.key), []byte(kv.val)))
|
|
require.Equal(t, []byte(kv.val), apple.Get([]byte(kv.key)))
|
|
}
|
|
|
|
got := make(map[string]string)
|
|
i := 0
|
|
// Error while iterating value keys.
|
|
err = apple.ForEach(func(key, val []byte) error {
|
|
if i == 2 {
|
|
return fmt.Errorf("error")
|
|
}
|
|
|
|
got[string(key)] = string(val)
|
|
i++
|
|
return nil
|
|
})
|
|
|
|
expected := map[string]string{
|
|
"banana": "",
|
|
"key1": "val1",
|
|
}
|
|
|
|
require.Equal(t, expected, got)
|
|
require.Error(t, err)
|
|
|
|
got = make(map[string]string)
|
|
i = 0
|
|
// Erro while iterating buckets.
|
|
err = apple.ForEach(func(key, val []byte) error {
|
|
if i == 3 {
|
|
return fmt.Errorf("error")
|
|
}
|
|
|
|
got[string(key)] = string(val)
|
|
i++
|
|
return nil
|
|
})
|
|
|
|
expected = map[string]string{
|
|
"banana": "",
|
|
"key1": "val1",
|
|
"key2": "val2",
|
|
}
|
|
|
|
require.Equal(t, expected, got)
|
|
require.Error(t, err)
|
|
return nil
|
|
})
|
|
|
|
require.Nil(t, err)
|
|
|
|
expected := map[string]string{
|
|
bkey("apple"): bval("apple"),
|
|
bkey("apple", "banana"): bval("apple", "banana"),
|
|
bkey("apple", "pear"): bval("apple", "pear"),
|
|
vkey("key1", "apple"): "val1",
|
|
vkey("key2", "apple"): "val2",
|
|
}
|
|
require.Equal(t, expected, f.Dump())
|
|
}
|
|
|
|
func TestBucketSequence(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := NewEtcdTestFixture(t)
|
|
defer f.Cleanup()
|
|
|
|
db, err := newEtcdBackend(f.BackendConfig())
|
|
require.NoError(t, err)
|
|
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
apple, err := tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, apple)
|
|
|
|
banana, err := apple.CreateBucket([]byte("banana"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, banana)
|
|
|
|
require.Equal(t, uint64(0), apple.Sequence())
|
|
require.Equal(t, uint64(0), banana.Sequence())
|
|
|
|
require.Nil(t, apple.SetSequence(math.MaxUint64))
|
|
require.Equal(t, uint64(math.MaxUint64), apple.Sequence())
|
|
|
|
for i := uint64(0); i < uint64(5); i++ {
|
|
s, err := apple.NextSequence()
|
|
require.Nil(t, err)
|
|
require.Equal(t, i, s)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
// TestKeyClash tests that one cannot create a bucket if a value with the same
|
|
// key exists and the same is true in reverse: that a value cannot be put if
|
|
// a bucket with the same key exists.
|
|
func TestKeyClash(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := NewEtcdTestFixture(t)
|
|
defer f.Cleanup()
|
|
|
|
db, err := newEtcdBackend(f.BackendConfig())
|
|
require.NoError(t, err)
|
|
|
|
// First:
|
|
// put: /apple/key -> val
|
|
// create bucket: /apple/banana
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
apple, err := tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, apple)
|
|
|
|
require.NoError(t, apple.Put([]byte("key"), []byte("val")))
|
|
|
|
banana, err := apple.CreateBucket([]byte("banana"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, banana)
|
|
|
|
return nil
|
|
})
|
|
|
|
require.Nil(t, err)
|
|
|
|
// Next try to:
|
|
// put: /apple/banana -> val => will fail (as /apple/banana is a bucket)
|
|
// create bucket: /apple/key => will fail (as /apple/key is a value)
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
apple, err := tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.Nil(t, err)
|
|
require.NotNil(t, apple)
|
|
|
|
require.Error(t,
|
|
walletdb.ErrIncompatibleValue,
|
|
apple.Put([]byte("banana"), []byte("val")),
|
|
)
|
|
|
|
b, err := apple.CreateBucket([]byte("key"))
|
|
require.Nil(t, b)
|
|
require.Error(t, walletdb.ErrIncompatibleValue, b)
|
|
|
|
b, err = apple.CreateBucketIfNotExists([]byte("key"))
|
|
require.Nil(t, b)
|
|
require.Error(t, walletdb.ErrIncompatibleValue, b)
|
|
|
|
return nil
|
|
})
|
|
|
|
require.Nil(t, err)
|
|
|
|
// Except that the only existing items in the db are:
|
|
// bucket: /apple
|
|
// bucket: /apple/banana
|
|
// value: /apple/key -> val
|
|
expected := map[string]string{
|
|
bkey("apple"): bval("apple"),
|
|
bkey("apple", "banana"): bval("apple", "banana"),
|
|
vkey("key", "apple"): "val",
|
|
}
|
|
require.Equal(t, expected, f.Dump())
|
|
|
|
}
|
|
|
|
// TestBucketCreateDelete tests that creating then deleting then creating a
|
|
// bucket suceeds.
|
|
func TestBucketCreateDelete(t *testing.T) {
|
|
t.Parallel()
|
|
f := NewEtcdTestFixture(t)
|
|
defer f.Cleanup()
|
|
|
|
db, err := newEtcdBackend(f.BackendConfig())
|
|
require.NoError(t, err)
|
|
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
apple, err := tx.CreateTopLevelBucket([]byte("apple"))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, apple)
|
|
|
|
banana, err := apple.CreateBucket([]byte("banana"))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, banana)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
apple := tx.ReadWriteBucket([]byte("apple"))
|
|
require.NotNil(t, apple)
|
|
require.NoError(t, apple.DeleteNestedBucket([]byte("banana")))
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = db.Update(func(tx walletdb.ReadWriteTx) error {
|
|
apple := tx.ReadWriteBucket([]byte("apple"))
|
|
require.NotNil(t, apple)
|
|
require.NoError(t, apple.Put([]byte("banana"), []byte("value")))
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
expected := map[string]string{
|
|
vkey("banana", "apple"): "value",
|
|
bkey("apple"): bval("apple"),
|
|
}
|
|
require.Equal(t, expected, f.Dump())
|
|
}
|