571 lines
14 KiB
Go
571 lines
14 KiB
Go
package orm
|
|
|
|
import (
|
|
stdErrors "errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/cosmos/cosmos-sdk/store/prefix"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
|
"github.com/cosmos/cosmos-sdk/x/group/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewIndexer(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
indexerFunc IndexerFunc
|
|
expectErr bool
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "nil indexer func",
|
|
indexerFunc: nil,
|
|
expectErr: true,
|
|
expectedErr: "Indexer func must not be nil",
|
|
},
|
|
{
|
|
name: "all not nil",
|
|
indexerFunc: func(interface{}) ([]interface{}, error) { return nil, nil },
|
|
expectErr: false,
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
indexer, err := NewIndexer(tc.indexerFunc)
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.expectedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, indexer)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewUniqueIndexer(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
indexerFunc UniqueIndexerFunc
|
|
expectErr bool
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "nil indexer func",
|
|
indexerFunc: nil,
|
|
expectErr: true,
|
|
expectedErr: "Indexer func must not be nil",
|
|
},
|
|
{
|
|
name: "all not nil",
|
|
indexerFunc: func(interface{}) (interface{}, error) { return nil, nil },
|
|
expectErr: false,
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
indexer, err := NewUniqueIndexer(tc.indexerFunc)
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.expectedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, indexer)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIndexerOnCreate(t *testing.T) {
|
|
var myRowID RowID = EncodeSequence(1)
|
|
|
|
specs := map[string]struct {
|
|
srcFunc IndexerFunc
|
|
expIndexKeys []interface{}
|
|
expRowIDs []RowID
|
|
expAddFuncCalled bool
|
|
expErr error
|
|
}{
|
|
"single key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(1)}, nil
|
|
},
|
|
expAddFuncCalled: true,
|
|
expIndexKeys: []interface{}{uint64(1)},
|
|
expRowIDs: []RowID{myRowID},
|
|
},
|
|
"multi key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(1), uint64(128)}, nil
|
|
},
|
|
expAddFuncCalled: true,
|
|
expIndexKeys: []interface{}{uint64(1), uint64(128)},
|
|
expRowIDs: []RowID{myRowID, myRowID},
|
|
},
|
|
"empty key in slice": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{[]byte{}}, nil
|
|
},
|
|
expAddFuncCalled: false,
|
|
},
|
|
"nil key in slice": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{nil}, nil
|
|
},
|
|
expErr: fmt.Errorf("type %T not allowed as key part", nil),
|
|
expAddFuncCalled: false,
|
|
},
|
|
"empty key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{}, nil
|
|
},
|
|
expAddFuncCalled: false,
|
|
},
|
|
"nil key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return nil, nil
|
|
},
|
|
expAddFuncCalled: false,
|
|
},
|
|
"error case": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return nil, stdErrors.New("test")
|
|
},
|
|
expErr: stdErrors.New("test"),
|
|
expAddFuncCalled: false,
|
|
},
|
|
}
|
|
for msg, spec := range specs {
|
|
t.Run(msg, func(t *testing.T) {
|
|
mockPolicy := &addFuncRecorder{}
|
|
idx, err := NewIndexer(spec.srcFunc)
|
|
require.NoError(t, err)
|
|
idx.addFunc = mockPolicy.add
|
|
|
|
err = idx.OnCreate(nil, myRowID, nil)
|
|
if spec.expErr != nil {
|
|
require.Equal(t, spec.expErr, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, spec.expIndexKeys, mockPolicy.secondaryIndexKeys)
|
|
assert.Equal(t, spec.expRowIDs, mockPolicy.rowIDs)
|
|
assert.Equal(t, spec.expAddFuncCalled, mockPolicy.called)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIndexerOnDelete(t *testing.T) {
|
|
myRowID := EncodeSequence(1)
|
|
|
|
var multiKeyIndex MultiKeyIndex
|
|
ctx := NewMockContext()
|
|
storeKey := sdk.NewKVStoreKey("test")
|
|
store := prefix.NewStore(ctx.KVStore(storeKey), []byte{multiKeyIndex.prefix})
|
|
|
|
specs := map[string]struct {
|
|
srcFunc IndexerFunc
|
|
expDeletedKeys []RowID
|
|
expErr error
|
|
}{
|
|
"single key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(1)}, nil
|
|
},
|
|
expDeletedKeys: []RowID{append(EncodeSequence(1), myRowID...)},
|
|
},
|
|
"multi key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(1), uint64(128)}, nil
|
|
},
|
|
expDeletedKeys: []RowID{
|
|
append(EncodeSequence(1), myRowID...),
|
|
append(EncodeSequence(128), myRowID...),
|
|
},
|
|
},
|
|
"empty key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{}, nil
|
|
},
|
|
},
|
|
"nil key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return nil, nil
|
|
},
|
|
},
|
|
"empty key in slice": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{[]byte{}}, nil
|
|
},
|
|
},
|
|
"nil key in slice": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{nil}, nil
|
|
},
|
|
expErr: fmt.Errorf("type %T not allowed as key part", nil),
|
|
},
|
|
"error case": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return nil, stdErrors.New("test")
|
|
},
|
|
expErr: stdErrors.New("test"),
|
|
},
|
|
}
|
|
for msg, spec := range specs {
|
|
t.Run(msg, func(t *testing.T) {
|
|
idx, err := NewIndexer(spec.srcFunc)
|
|
require.NoError(t, err)
|
|
|
|
if spec.expErr == nil {
|
|
err = idx.OnCreate(store, myRowID, nil)
|
|
require.NoError(t, err)
|
|
for _, key := range spec.expDeletedKeys {
|
|
require.Equal(t, true, store.Has(key))
|
|
}
|
|
}
|
|
|
|
err = idx.OnDelete(store, myRowID, nil)
|
|
if spec.expErr != nil {
|
|
require.Equal(t, spec.expErr, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
for _, key := range spec.expDeletedKeys {
|
|
require.Equal(t, false, store.Has(key))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestIndexerOnUpdate(t *testing.T) {
|
|
myRowID := EncodeSequence(1)
|
|
|
|
var multiKeyIndex MultiKeyIndex
|
|
ctx := NewMockContext()
|
|
storeKey := sdk.NewKVStoreKey("test")
|
|
store := prefix.NewStore(ctx.KVStore(storeKey), []byte{multiKeyIndex.prefix})
|
|
|
|
specs := map[string]struct {
|
|
srcFunc IndexerFunc
|
|
expAddedKeys []RowID
|
|
expDeletedKeys []RowID
|
|
expErr error
|
|
addFunc func(sdk.KVStore, interface{}, RowID) error
|
|
}{
|
|
"single key - same key, no update": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(1)}, nil
|
|
},
|
|
},
|
|
"single key - different key, replaced": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
keys := []uint64{1, 2}
|
|
return []interface{}{keys[value.(int)]}, nil
|
|
},
|
|
expAddedKeys: []RowID{
|
|
append(EncodeSequence(2), myRowID...),
|
|
},
|
|
expDeletedKeys: []RowID{
|
|
append(EncodeSequence(1), myRowID...),
|
|
},
|
|
},
|
|
"multi key - same key, no update": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(1), uint64(2)}, nil
|
|
},
|
|
},
|
|
"multi key - replaced": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
keys := []uint64{1, 2, 3, 4}
|
|
return []interface{}{keys[value.(int)], keys[value.(int)+2]}, nil
|
|
},
|
|
expAddedKeys: []RowID{
|
|
append(EncodeSequence(2), myRowID...),
|
|
append(EncodeSequence(4), myRowID...),
|
|
},
|
|
expDeletedKeys: []RowID{
|
|
append(EncodeSequence(1), myRowID...),
|
|
append(EncodeSequence(3), myRowID...),
|
|
},
|
|
},
|
|
"empty key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{}, nil
|
|
},
|
|
},
|
|
"nil key": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return nil, nil
|
|
},
|
|
},
|
|
"empty key in slice": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{[]byte{}}, nil
|
|
},
|
|
},
|
|
"nil key in slice": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return []interface{}{nil}, nil
|
|
},
|
|
expErr: fmt.Errorf("type %T not allowed as key part", nil),
|
|
},
|
|
"error case with new value": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
return nil, stdErrors.New("test")
|
|
},
|
|
expErr: stdErrors.New("test"),
|
|
},
|
|
"error case with old value": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
var err error
|
|
if value.(int)%2 == 1 {
|
|
err = stdErrors.New("test")
|
|
}
|
|
return []interface{}{uint64(1)}, err
|
|
},
|
|
expErr: stdErrors.New("test"),
|
|
},
|
|
"error case on persisting new keys": {
|
|
srcFunc: func(value interface{}) ([]interface{}, error) {
|
|
keys := []uint64{1, 2}
|
|
return []interface{}{keys[value.(int)]}, nil
|
|
},
|
|
addFunc: func(_ sdk.KVStore, _ interface{}, _ RowID) error {
|
|
return stdErrors.New("test")
|
|
},
|
|
expErr: stdErrors.New("test"),
|
|
},
|
|
}
|
|
for msg, spec := range specs {
|
|
t.Run(msg, func(t *testing.T) {
|
|
idx, err := NewIndexer(spec.srcFunc)
|
|
require.NoError(t, err)
|
|
|
|
if spec.expErr == nil {
|
|
err = idx.OnCreate(store, myRowID, 0)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if spec.addFunc != nil {
|
|
idx.addFunc = spec.addFunc
|
|
}
|
|
err = idx.OnUpdate(store, myRowID, 1, 0)
|
|
if spec.expErr != nil {
|
|
require.Equal(t, spec.expErr, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
for _, key := range spec.expAddedKeys {
|
|
require.Equal(t, true, store.Has(key))
|
|
}
|
|
for _, key := range spec.expDeletedKeys {
|
|
require.Equal(t, false, store.Has(key))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUniqueKeyAddFunc(t *testing.T) {
|
|
myRowID := EncodeSequence(1)
|
|
presetKeyPart := []byte("my-preset-key")
|
|
presetKey := append(AddLengthPrefix(presetKeyPart), myRowID...)
|
|
|
|
specs := map[string]struct {
|
|
srcKey []byte
|
|
expErr *sdkerrors.Error
|
|
expExistingEntry []byte
|
|
}{
|
|
|
|
"create when not exists": {
|
|
srcKey: []byte("my-index-key"),
|
|
expExistingEntry: append(AddLengthPrefix([]byte("my-index-key")), myRowID...),
|
|
},
|
|
"error when exists already": {
|
|
srcKey: presetKeyPart,
|
|
expErr: errors.ErrORMUniqueConstraint,
|
|
},
|
|
"nil key not allowed": {
|
|
srcKey: nil,
|
|
expErr: errors.ErrORMInvalidArgument,
|
|
},
|
|
"empty key not allowed": {
|
|
srcKey: []byte{},
|
|
expErr: errors.ErrORMInvalidArgument,
|
|
},
|
|
}
|
|
for msg, spec := range specs {
|
|
t.Run(msg, func(t *testing.T) {
|
|
storeKey := sdk.NewKVStoreKey("test")
|
|
store := NewMockContext().KVStore(storeKey)
|
|
store.Set(presetKey, []byte{})
|
|
|
|
err := uniqueKeysAddFunc(store, spec.srcKey, myRowID)
|
|
require.True(t, spec.expErr.Is(err))
|
|
if spec.expErr != nil {
|
|
return
|
|
}
|
|
assert.True(t, store.Has(spec.expExistingEntry), "not found")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMultiKeyAddFunc(t *testing.T) {
|
|
myRowID := EncodeSequence(1)
|
|
presetKeyPart := []byte("my-preset-key")
|
|
presetKey := append(AddLengthPrefix(presetKeyPart), myRowID...)
|
|
|
|
specs := map[string]struct {
|
|
srcKey []byte
|
|
expErr *sdkerrors.Error
|
|
expExistingEntry []byte
|
|
}{
|
|
|
|
"create when not exists": {
|
|
srcKey: []byte("my-index-key"),
|
|
expExistingEntry: append(AddLengthPrefix([]byte("my-index-key")), myRowID...),
|
|
},
|
|
"noop when exists already": {
|
|
srcKey: presetKeyPart,
|
|
expExistingEntry: presetKey,
|
|
},
|
|
"nil key not allowed": {
|
|
srcKey: nil,
|
|
expErr: errors.ErrORMInvalidArgument,
|
|
},
|
|
"empty key not allowed": {
|
|
srcKey: []byte{},
|
|
expErr: errors.ErrORMInvalidArgument,
|
|
},
|
|
}
|
|
for msg, spec := range specs {
|
|
t.Run(msg, func(t *testing.T) {
|
|
storeKey := sdk.NewKVStoreKey("test")
|
|
store := NewMockContext().KVStore(storeKey)
|
|
store.Set(presetKey, []byte{})
|
|
|
|
err := multiKeyAddFunc(store, spec.srcKey, myRowID)
|
|
require.True(t, spec.expErr.Is(err))
|
|
if spec.expErr != nil {
|
|
return
|
|
}
|
|
assert.True(t, store.Has(spec.expExistingEntry))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDifference(t *testing.T) {
|
|
specs := map[string]struct {
|
|
srcA []interface{}
|
|
srcB []interface{}
|
|
expResult []interface{}
|
|
expErr bool
|
|
}{
|
|
"all of A": {
|
|
srcA: []interface{}{"a", "b"},
|
|
srcB: []interface{}{"c"},
|
|
expResult: []interface{}{"a", "b"},
|
|
},
|
|
"A - B": {
|
|
srcA: []interface{}{"a", "b"},
|
|
srcB: []interface{}{"b", "c", "d"},
|
|
expResult: []interface{}{"a"},
|
|
},
|
|
"type in A not allowed": {
|
|
srcA: []interface{}{1},
|
|
srcB: []interface{}{"b", "c", "d"},
|
|
expErr: true,
|
|
},
|
|
"type in B not allowed": {
|
|
srcA: []interface{}{"b", "c", "d"},
|
|
srcB: []interface{}{1},
|
|
expErr: true,
|
|
},
|
|
}
|
|
for msg, spec := range specs {
|
|
t.Run(msg, func(t *testing.T) {
|
|
got, err := difference(spec.srcA, spec.srcB)
|
|
if spec.expErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
assert.Equal(t, spec.expResult, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPruneEmptyKeys(t *testing.T) {
|
|
specs := map[string]struct {
|
|
srcFunc IndexerFunc
|
|
expResult []interface{}
|
|
expError error
|
|
}{
|
|
"non empty": {
|
|
srcFunc: func(v interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(0), uint64(1)}, nil
|
|
},
|
|
expResult: []interface{}{uint64(0), uint64(1)},
|
|
},
|
|
"empty": {
|
|
srcFunc: func(v interface{}) ([]interface{}, error) {
|
|
return []interface{}{}, nil
|
|
},
|
|
expResult: []interface{}{},
|
|
},
|
|
"nil": {
|
|
srcFunc: func(v interface{}) ([]interface{}, error) {
|
|
return nil, nil
|
|
},
|
|
},
|
|
"empty in the beginning": {
|
|
srcFunc: func(v interface{}) ([]interface{}, error) {
|
|
return []interface{}{[]byte{}, uint64(0), uint64(1)}, nil
|
|
},
|
|
expResult: []interface{}{uint64(0), uint64(1)},
|
|
},
|
|
"empty in the middle": {
|
|
srcFunc: func(v interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(0), []byte{}, uint64(1)}, nil
|
|
},
|
|
expResult: []interface{}{uint64(0), uint64(1)},
|
|
},
|
|
"empty at the end": {
|
|
srcFunc: func(v interface{}) ([]interface{}, error) {
|
|
return []interface{}{uint64(0), uint64(1), []byte{}}, nil
|
|
},
|
|
expResult: []interface{}{uint64(0), uint64(1)},
|
|
},
|
|
"error passed": {
|
|
srcFunc: func(v interface{}) ([]interface{}, error) {
|
|
return nil, stdErrors.New("test")
|
|
},
|
|
expError: stdErrors.New("test"),
|
|
},
|
|
}
|
|
for msg, spec := range specs {
|
|
t.Run(msg, func(t *testing.T) {
|
|
r, err := pruneEmptyKeys(spec.srcFunc)(nil)
|
|
require.Equal(t, spec.expError, err)
|
|
if spec.expError != nil {
|
|
return
|
|
}
|
|
assert.Equal(t, spec.expResult, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
type addFuncRecorder struct {
|
|
secondaryIndexKeys []interface{}
|
|
rowIDs []RowID
|
|
called bool
|
|
}
|
|
|
|
func (c *addFuncRecorder) add(_ sdk.KVStore, key interface{}, rowID RowID) error {
|
|
c.secondaryIndexKeys = append(c.secondaryIndexKeys, key)
|
|
c.rowIDs = append(c.rowIDs, rowID)
|
|
c.called = true
|
|
return nil
|
|
}
|