chestnut/chestnut_test.go

719 lines
20 KiB
Go

package chestnut
import (
"errors"
"path/filepath"
"reflect"
"sort"
"testing"
"github.com/google/uuid"
"github.com/jrapoport/chestnut/encoding/compress"
"github.com/jrapoport/chestnut/encoding/compress/zstd"
"github.com/jrapoport/chestnut/encryptor"
"github.com/jrapoport/chestnut/encryptor/aes"
"github.com/jrapoport/chestnut/encryptor/crypto"
"github.com/jrapoport/chestnut/log"
"github.com/jrapoport/chestnut/storage"
"github.com/jrapoport/chestnut/storage/bolt"
"github.com/jrapoport/chestnut/storage/nuts"
"github.com/jrapoport/chestnut/value"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type TObject struct {
ValueA string `json:"value_a"`
ValueB int `json:"value_b"`
}
type THash struct {
TObject
HashValueA string `json:"hash_value_a,hash"`
HashValueB int `json:"hash_value_b,hash"`
}
type TSecure struct {
TObject
SecureValueA string `json:"sparse_value_a,secure"`
SecureValueB int `json:"sparse_value_b,secure"`
}
type TAll struct {
TObject
Hash THash
Secure TSecure
AllValueA string `json:"all_value_a,secure,hash"`
AllValueB int `json:"all_value_b,secure,hash"`
}
type testCase struct {
key string
value string
err assert.ErrorAssertionFunc
assertHas assert.BoolAssertionFunc
}
var (
testName = "test-namespace"
testValue = "i-am-plaintext"
textSecret = crypto.TextSecret("i-am-a-good-secret")
encryptorOpt = WithAES(crypto.Key256, aes.CFB, textSecret)
)
var lorumIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu consequat ac
felis donec et odio pellentesqu diam. Hac habitasse platea dictumst quisque
sagittis purus. Risus at ultrices mi tempus imperdiet nulla malesuada
pellentesque. Vitae justo eget magna fermentum iaculis eu non diam phasellus.
Cursus risus at ultrices mi tempus imperdiet. Ante metus dictum at tempor
commodo. Accumsan lacus vel facilisis volutpat est velit egestas. Dolor sed
viverra ipsum nunc aliquet bibendum enim facilisis. Tristique risus nec feugiat
in. Feugiat nisl pretium fusce id velit ut tortor pretium. Eget magna fermentum
iaculis eu. Velit laoreet id donec ultrices tincidunt. Tristique senectus et
netus et malesuada fames ac turpis egestas. Diam phasellus vestibulum lorem sed
risus ultricies tristique. Cursus mattis molestie a iaculis. Sem nulla pharetra
diam sit amet nisl suscipit adipiscing bibendum. Viverra justo nec ultrices dui
sapien eget. Ornare arcu dui vivamus arcu felis. Egestas integer eget aliquet
nibh praesent. Risus feugiat in ante metus dictum at tempor commodo. Id ornare
arcu odio ut sem. Tincidunt dui ut ornare lectus. Sagittis orci a scelerisque
purus. Suscipit adipiscing bibendum est ultricies integer quis. Libero nunc
consequat interdum varius sit amet mattis vulputate. Euismod lacinia at quis
risus sed vulputate. Molestie at elementum eu facilisis sed odio morbi quis.
Nunc sed augue lacus viverra vitae congue eu consequat ac. Interdum velit
euismod in pellentesque. Mi sit amet mauris commodo quis imperdiet massa
tincidunt. Eget magna fermentum iaculis eu. Metus aliquam eleifend mi in nulla
posuere sollicitudin. Nisi est sit amet facilisis magna. Tellus in hac habitasse
platea dictumst. Venenatis tellus in metus vulputate eu scelerisque. Feugiat sed
lectus vestibulum mattis. Sit amet nisl suscipit adipiscing bibendum est
ultricies. Ipsum nunc aliquet bibendum enim facilisis gravida neque. Duis
tristique sollicitudin nibh sit amet commodo. Purus in massa tempor nec. Eget
aliquet nibh praesent tristique magna sit amet. Mauris in aliquam semf ringilla`
var valOut = testValue
var objectSrc = TObject{
ValueA: testValue,
ValueB: 42,
}
var objOut = objectSrc
var hashSrc = THash{
TObject: objectSrc,
HashValueA: testValue,
HashValueB: 1600,
}
var hashOut = THash{
TObject: objOut,
HashValueA: "sha256:0fdabf2262ab284503a700b876994fc95ee4690133db96acfb5f9ea526d71e94",
HashValueB: 1600,
}
var secureSrc = TSecure{
TObject: objectSrc,
SecureValueA: testValue,
SecureValueB: 1337,
}
var secureOut = TSecure{
TObject: objOut,
SecureValueA: "i-am-plaintext",
SecureValueB: 1337,
}
var secureSparse = TSecure{
TObject: objOut,
SecureValueA: "",
SecureValueB: 0,
}
var allSrc = TAll{
TObject: objectSrc,
Hash: hashSrc,
Secure: secureSrc,
AllValueA: "i-am-a-random-string",
AllValueB: 0xbeef,
}
var allOut = TAll{
TObject: objOut,
Hash: hashOut,
Secure: secureOut,
AllValueA: "sha256:50d5a31ee8353543fe8d6c0de2c9d5e5e2cdb7b973c4f9c25f99fcdf41bd5eec",
AllValueB: 0xbeef,
}
var allSparse = TAll{
TObject: objOut,
Hash: hashOut,
Secure: secureSparse,
AllValueA: "",
AllValueB: 0,
}
type objTest struct {
key string
src interface{}
dst interface{}
out interface{}
spr interface{}
err assert.ErrorAssertionFunc
}
var putTests = []testCase{
{"", "", assert.Error, assert.False},
{"a", "", assert.Error, assert.False},
{"b", testValue, assert.NoError, assert.True},
{"c/c", testValue, assert.NoError, assert.True},
{".d", testValue, assert.NoError, assert.True},
{newKey(), testValue, assert.NoError, assert.True},
}
var tests = append(putTests,
testCase{"not-found", "", assert.Error, assert.False},
)
var objTests = []objTest{
{"", nil, nil, nil, nil, assert.Error},
{"a", nil, nil, nil, nil, assert.Error},
{"b", testValue, new(string), &valOut, &valOut, assert.NoError},
{newKey(), testValue, new(string), &valOut, &valOut, assert.NoError},
{newKey(), objectSrc, &TObject{}, &objOut, &objOut, assert.NoError},
{newKey(), hashSrc, &THash{}, &hashOut, &hashOut, assert.NoError},
{newKey(), secureSrc, &TSecure{}, &secureOut, &secureSparse, assert.NoError},
{newKey(), allSrc, &TAll{}, &allOut, &allSparse, assert.NoError},
}
func newKey() string {
return uuid.New().String()
}
func nutsStore(t *testing.T, path string) storage.Storage {
store := nuts.NewStore(path)
assert.NotNil(t, store)
return store
}
func boltStore(t *testing.T, path string) storage.Storage {
store := bolt.NewStore(path)
assert.NotNil(t, store)
return store
}
type StoreFunc = func(t *testing.T, path string) storage.Storage
type ChestnutTestSuite struct {
suite.Suite
storeFunc StoreFunc
cn *Chestnut
}
func TestChestnut(t *testing.T) {
testStores := []StoreFunc{nutsStore, boltStore}
for _, test := range testStores {
ts := new(ChestnutTestSuite)
ts.storeFunc = test
suite.Run(t, ts)
}
}
func (ts *ChestnutTestSuite) SetupTest() {
store := ts.storeFunc(ts.T(), ts.T().TempDir())
assert.NotNil(ts.T(), store)
ts.cn = NewChestnut(store, encryptorOpt)
assert.NotNil(ts.T(), ts.cn)
err := ts.cn.Open()
assert.NoError(ts.T(), err)
}
func (ts *ChestnutTestSuite) TearDownTest() {
err := ts.cn.Close()
assert.NoError(ts.T(), err)
}
func (ts *ChestnutTestSuite) BeforeTest(_, testName string) {
switch testName {
case "TestChestnut_Put",
"TestChestnut_List",
"TestChestnut_Save":
break
case "TestChestnut_Load",
"TestChestnut_Sparse":
ts.TestChestnut_Save()
break
case "TestChestnut_LoadKeyed",
"TestChestnut_SparseKeyed":
ts.TestChestnut_SaveKeyed()
break
default:
ts.TestChestnut_Put()
}
}
func (ts *ChestnutTestSuite) TestChestnut_Put() {
for i, test := range putTests {
err := ts.cn.Put(testName, []byte(test.key), []byte(test.value))
test.err(ts.T(), err, "%d test key: %s", i, test.key)
}
}
func (ts *ChestnutTestSuite) TestChestnut_Get() {
for i, test := range tests {
value, err := ts.cn.Get(testName, []byte(test.key))
test.err(ts.T(), err, "%d test key: %s", i, test.key)
assert.Equal(ts.T(), test.value, string(value),
"%d test key: %s", i, test.key)
}
}
func (ts *ChestnutTestSuite) TestChestnut_Save() {
for i, test := range objTests {
err := ts.cn.Save(testName, []byte(test.key), test.src)
test.err(ts.T(), err, "%d test key: %s", i, test.key)
}
}
func (ts *ChestnutTestSuite) TestChestnut_Load() {
for i, test := range objTests {
if test.dst == nil {
continue
}
typ := reflect.ValueOf(test.dst).Elem().Type()
ptr := reflect.New(typ).Interface()
err := ts.cn.Load(testName, []byte(test.key), ptr)
test.err(ts.T(), err, "%d test key: %s", i, test.key)
assert.Equal(ts.T(), test.out, ptr)
}
}
func (ts *ChestnutTestSuite) TestChestnut_Sparse() {
for i, test := range objTests {
if test.dst == nil {
continue
}
typ := reflect.ValueOf(test.dst).Elem().Type()
ptr := reflect.New(typ).Interface()
err := ts.cn.Sparse(testName, []byte(test.key), ptr)
test.err(ts.T(), err, "%d test key: %s", i, test.key)
assert.Equal(ts.T(), test.spr, ptr)
}
}
var keyedObj = value.NewSecureValue(newKey(), []byte(lorumIpsum))
func (ts *ChestnutTestSuite) TestChestnut_SaveKeyed() {
objs := []struct {
in *value.Secure
err assert.ErrorAssertionFunc
}{
{nil, assert.Error},
{&value.Secure{}, assert.Error},
{keyedObj, assert.NoError},
}
for i, test := range objs {
err := ts.cn.SaveKeyed(test.in)
test.err(ts.T(), err, "%d test", i)
}
}
func (ts *ChestnutTestSuite) TestChestnut_LoadKeyed() {
objs := []struct {
in *value.Secure
out *value.Secure
err assert.ErrorAssertionFunc
}{
{nil, nil, assert.Error},
{&value.Secure{}, nil, assert.Error},
{&value.Secure{ID: value.ID{ID: "not-found"}}, nil, assert.Error},
{&value.Secure{ID: keyedObj.ID}, keyedObj, assert.NoError},
}
for i, test := range objs {
err := ts.cn.LoadKeyed(test.in)
test.err(ts.T(), err, "%d test", i)
if err == nil {
assert.Equal(ts.T(), test.out, test.in)
}
}
}
func (ts *ChestnutTestSuite) TestChestnut_SparseKeyed() {
sparse := &value.Secure{
ID: keyedObj.ID,
Metadata: map[string]interface{}{},
}
objs := []struct {
in *value.Secure
out *value.Secure
err assert.ErrorAssertionFunc
}{
{nil, nil, assert.Error},
{&value.Secure{}, nil, assert.Error},
{in: &value.Secure{ID: value.ID{ID: "not-found"}}, err: assert.Error},
{&value.Secure{ID: keyedObj.ID}, sparse, assert.NoError},
}
for i, test := range objs {
err := ts.cn.SparseKeyed(test.in)
test.err(ts.T(), err, "%d test", i)
if err == nil {
assert.Equal(ts.T(), test.out, test.in)
}
}
}
func (ts *ChestnutTestSuite) TestChestnut_Has() {
for i, test := range tests {
has, _ := ts.cn.Has(testName, []byte(test.key))
test.assertHas(ts.T(), has, "%d test key: %s", i, test.key)
}
}
func (ts *ChestnutTestSuite) TestChestnut_List() {
const listLen = 100
list := make([]string, listLen)
for i := 0; i < listLen; i++ {
list[i] = uuid.New().String()
err := ts.cn.Put(testName, []byte(list[i]), []byte(testValue))
assert.NoError(ts.T(), err)
}
keys, err := ts.cn.List(testName)
assert.NoError(ts.T(), err)
assert.Len(ts.T(), keys, listLen)
// put both lists in the same order so we can compare them
strKeys := make([]string, len(keys))
for i, k := range keys {
strKeys[i] = string(k)
}
sort.Strings(list)
sort.Strings(strKeys)
assert.Equal(ts.T(), list, strKeys)
}
func (ts *ChestnutTestSuite) TestChestnut_Delete() {
var deleteTests = []struct {
key string
err assert.ErrorAssertionFunc
}{
{"", assert.Error},
{"a", assert.NoError},
{"b", assert.NoError},
{"c/c", assert.NoError},
{".d", assert.NoError},
{"eee", assert.NoError},
{"not-found", assert.NoError},
}
for i, test := range deleteTests {
err := ts.cn.Delete(testName, []byte(test.key))
test.err(ts.T(), err, "%d test key: %s", i, test.key)
}
}
func (ts *ChestnutTestSuite) TestStore_Export() {
err := ts.cn.Export(ts.T().TempDir())
assert.NoError(ts.T(), err)
}
func (ts *ChestnutTestSuite) TestStore_SecureEntry() {
const (
testKey = "hello"
testValue = "world"
testData = "foobar"
)
entries := make([]*value.Secure, 20)
for i := range entries {
e := value.NewSecureValue(uuid.New().String(), []byte(testData))
e.SetMetadata(testKey, testValue)
entries[i] = e
}
for _, e := range entries {
err := ts.cn.Save(testName, e.Key(), e)
assert.NoError(ts.T(), err)
}
for _, e := range entries {
spr := &value.Secure{}
err := ts.cn.Sparse(testName, e.Key(), &spr)
assert.NoError(ts.T(), err)
assert.Empty(ts.T(), spr.Data)
assert.Equal(ts.T(), testValue, spr.GetMetadata(testKey))
}
for _, e := range entries {
spr := &value.Secure{}
err := ts.cn.Load(testName, e.Key(), &spr)
assert.NoError(ts.T(), err)
assert.Equal(ts.T(), testData, string(spr.Data))
assert.Equal(ts.T(), testValue, spr.GetMetadata(testKey))
}
}
func (ts *ChestnutTestSuite) TestChestnut_OverwritesDisabled() {
ts.testOptionDisableOverwrites(false)
}
func (ts *ChestnutTestSuite) TestChestnut_OverwritesEnabled() {
ts.testOptionDisableOverwrites(true)
}
func (ts *ChestnutTestSuite) testOptionDisableOverwrites(enabled bool) {
key := newKey()
path := filepath.Join(ts.T().TempDir())
store := ts.storeFunc(ts.T(), path)
assert.NotNil(ts.T(), store)
opts := []ChestOption{
encryptorOpt,
}
assertErr := assert.NoError
if !enabled {
assertErr = assert.Error
opts = append(opts, OverwritesForbidden())
}
cn := NewChestnut(store, opts...)
assert.NotNil(ts.T(), cn)
assert.Equal(ts.T(), enabled, cn.opts.overwrites)
defer func() {
err := cn.Close()
assert.NoError(ts.T(), err)
}()
err := cn.Open()
assert.NoError(ts.T(), err)
err = cn.Put(testName, []byte(key), []byte(testValue))
assert.NoError(ts.T(), err)
// this should fail with an error if overwrites are disabled
err = cn.Put(testName, []byte(key), []byte(testValue))
assertErr(ts.T(), err)
}
func (ts *ChestnutTestSuite) TestChestnut_ChainedEncryptor() {
var operation = "encrypting"
// initialize a keystore with a chained encryptor
openSecret := func(s crypto.Secret) []byte {
ts.T().Logf("%s with secret %s", operation, s.ID())
return []byte(s.ID())
}
managedSecret := crypto.NewManagedSecret(uuid.New().String(), "i-am-a-managed-secret")
secureSecret1 := crypto.NewSecureSecret(uuid.New().String(), openSecret)
secureSecret2 := crypto.NewSecureSecret(uuid.New().String(), openSecret)
encryptorChainOpt := WithEncryptorChain(
encryptor.NewAESEncryptor(crypto.Key128, aes.CFB, secureSecret1),
encryptor.NewAESEncryptor(crypto.Key192, aes.CTR, managedSecret),
encryptor.NewAESEncryptor(crypto.Key256, aes.GCM, secureSecret2),
)
path := ts.T().TempDir()
store := ts.storeFunc(ts.T(), path)
assert.NotNil(ts.T(), store)
cn := NewChestnut(store, encryptorChainOpt)
assert.NotNil(ts.T(), cn)
defer func() {
err := cn.Close()
assert.NoError(ts.T(), err)
}()
err := cn.Open()
assert.NoError(ts.T(), err)
key := newKey()
err = cn.Put(testName, []byte(key), []byte(testValue))
assert.NoError(ts.T(), err)
operation = "decrypting"
v, err := cn.Get(testName, []byte(key))
assert.NotEmpty(ts.T(), v)
assert.NoError(ts.T(), err)
assert.Equal(ts.T(), []byte(testValue), v)
err = cn.Delete(testName, []byte(key))
assert.NoError(ts.T(), err)
e := value.NewSecureValue(uuid.New().String(), []byte(testValue))
err = cn.Save(testName, []byte(key), e)
assert.NoError(ts.T(), err)
se1 := &value.Secure{}
err = cn.Sparse(testName, []byte(key), se1)
assert.NoError(ts.T(), err)
se2 := &value.Secure{}
err = cn.Load(testName, []byte(key), se2)
assert.NoError(ts.T(), err)
}
func (ts *ChestnutTestSuite) TestChestnut_Compression() {
compOpt := WithCompression(compress.Zstd)
key := newKey()
path := filepath.Join(ts.T().TempDir())
store := ts.storeFunc(ts.T(), path)
assert.NotNil(ts.T(), store)
cn := NewChestnut(store, encryptorOpt, compOpt)
assert.NotNil(ts.T(), cn)
defer func() {
err := cn.Close()
assert.NoError(ts.T(), err)
}()
err := cn.Open()
assert.NoError(ts.T(), err)
err = cn.Put(testName, []byte(key), []byte(lorumIpsum))
assert.NoError(ts.T(), err)
val, err := cn.Get(testName, []byte(key))
assert.NoError(ts.T(), err)
assert.Equal(ts.T(), lorumIpsum, string(val))
}
func (ts *ChestnutTestSuite) TestChestnut_Compressors() {
compOpt := WithCompressors(zstd.Compress, zstd.Decompress)
key := newKey()
path := filepath.Join(ts.T().TempDir())
store := ts.storeFunc(ts.T(), path)
assert.NotNil(ts.T(), store)
cn := NewChestnut(store, encryptorOpt, compOpt)
assert.NotNil(ts.T(), cn)
defer func() {
err := cn.Close()
assert.NoError(ts.T(), err)
}()
err := cn.Open()
assert.NoError(ts.T(), err)
err = cn.Put(testName, []byte(key), []byte(lorumIpsum))
assert.NoError(ts.T(), err)
val, err := cn.Get(testName, []byte(key))
assert.NoError(ts.T(), err)
assert.Equal(ts.T(), lorumIpsum, string(val))
}
func (ts *ChestnutTestSuite) TestChestnut_OpenErr() {
cn := &Chestnut{}
err := cn.Open()
assert.Error(ts.T(), err)
}
func (ts *ChestnutTestSuite) TestChestnut_SetLogger() {
path := ts.T().TempDir()
store := ts.storeFunc(ts.T(), path)
assert.NotNil(ts.T(), store)
cn := NewChestnut(store, encryptorOpt)
logTests := []log.Logger{
nil,
log.NewZapLoggerWithLevel(log.DebugLevel),
}
for _, test := range logTests {
cn.SetLogger(test)
err := cn.Open()
assert.NoError(ts.T(), err)
err = cn.Close()
assert.NoError(ts.T(), err)
}
}
func (ts *ChestnutTestSuite) TestChestnut_WithLogger() {
levels := []log.Level{
log.DebugLevel,
log.InfoLevel,
log.WarnLevel,
log.ErrorLevel,
log.PanicLevel,
}
type LoggerOpt func(log.Level) ChestOption
logOpts := []LoggerOpt{
WithLogrusLogger,
WithStdLogger,
WithZapLogger,
}
path := ts.T().TempDir()
store := ts.storeFunc(ts.T(), path)
assert.NotNil(ts.T(), store)
for _, level := range levels {
for _, logOpt := range logOpts {
opt := logOpt(level)
cn := NewChestnut(store, encryptorOpt, opt)
err := cn.Open()
assert.NoError(ts.T(), err)
err = cn.Close()
assert.NoError(ts.T(), err)
}
}
}
func (ts *ChestnutTestSuite) TestChestnut_BadConfig() {
store := ts.storeFunc(ts.T(), ts.T().TempDir())
assert.Panics(ts.T(), func() {
_ = NewChestnut(nil, encryptorOpt)
})
assert.Panics(ts.T(), func() {
_ = NewChestnut(store)
})
assert.Panics(ts.T(), func() {
_ = NewChestnut(store, encryptorOpt, WithCompression("X"))
})
assert.Panics(ts.T(), func() {
_ = NewChestnut(store, encryptorOpt, WithCompressors(nil, nil))
})
assert.Panics(ts.T(), func() {
_ = NewChestnut(store, encryptorOpt, WithCompressors(compress.PassthroughCompressor, nil))
})
assert.Panics(ts.T(), func() {
_ = NewChestnut(store, encryptorOpt, WithCompressors(nil, compress.PassthroughDecompressor))
})
}
type badEncryptor struct {}
func (b badEncryptor) ID() string {
return "a"
}
func (b badEncryptor) Name() string {
return "a"
}
func (b badEncryptor) Encrypt([]byte) ([]byte, error) {
return nil, errors.New("an error")
}
func (b badEncryptor) Decrypt([]byte) ([]byte,error) {
return nil, errors.New("an error")
}
var _ crypto.Encryptor = (*badEncryptor)(nil)
func (ts *ChestnutTestSuite) TestChestnut_BadEncryptor() {
var testGood = []byte("test-good")
var testBad = []byte("test-bad")
badCompress := func(data []byte) (compressed []byte, err error) {
return nil, errors.New("error")
}
store := ts.storeFunc(ts.T(), ts.T().TempDir())
assert.Panics(ts.T(), func() {
_ = NewChestnut(store, WithEncryptor(nil))
})
cn := NewChestnut(store, encryptorOpt)
err := cn.Open()
assert.NoError(ts.T(), err)
err = cn.Put(testName, testGood, testGood)
assert.NoError(ts.T(), err)
err = cn.Close()
assert.NoError(ts.T(), err)
cn = NewChestnut(store, WithEncryptor(&badEncryptor{}))
err = cn.Open()
assert.NoError(ts.T(), err)
err = cn.Put(testName, testBad, testBad)
assert.Error(ts.T(), err)
_, err = cn.Get(testName, testGood)
assert.Error(ts.T(), err)
err = cn.Close()
assert.NoError(ts.T(), err)
compOpt := WithCompressors(compress.PassthroughCompressor, compress.PassthroughDecompressor)
cn = NewChestnut(store, encryptorOpt, compOpt)
err = cn.Open()
assert.NoError(ts.T(), err)
err = cn.Put(testName, testGood, testGood)
assert.NoError(ts.T(), err)
err = cn.Close()
assert.NoError(ts.T(), err)
cn = NewChestnut(store, encryptorOpt, WithCompressors(badCompress, badCompress))
err = cn.Open()
assert.NoError(ts.T(), err)
err = cn.Put(testName, testBad, testBad)
assert.Error(ts.T(), err)
assert.Error(ts.T(), err)
_, err = cn.Get(testName, testGood)
assert.Error(ts.T(), err)
err = cn.Close()
assert.NoError(ts.T(), err)
}