From 42630eee9d30c342568ca7dff619ba1bb0dc336f Mon Sep 17 00:00:00 2001 From: Derek Trider Date: Thu, 13 Feb 2020 11:10:01 -0500 Subject: [PATCH] feat: Implement CouchDB Store Signed-off-by: Derek Trider --- go.mod | 14 +- go.sum | 40 ++++ pkg/storage/couchdb/couchdbstore.go | 204 ++++++++++++++++ pkg/storage/couchdb/couchdbstore_test.go | 292 +++++++++++++++++++++++ pkg/storage/memstore/memstore.go | 37 +-- pkg/storage/memstore/memstore_test.go | 105 +++++--- pkg/storage/store.go | 5 +- scripts/check_unit.sh | 47 +++- 8 files changed, 688 insertions(+), 56 deletions(-) create mode 100644 pkg/storage/couchdb/couchdbstore.go create mode 100644 pkg/storage/couchdb/couchdbstore_test.go diff --git a/go.mod b/go.mod index 901705c..992ccc2 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,16 @@ module github.com/trustbloc/edge-core go 1.13 -require github.com/stretchr/testify v1.4.0 +require ( + github.com/flimzy/diff v0.1.6 // indirect + github.com/flimzy/testy v0.1.16 // indirect + github.com/go-kivik/couchdb v2.0.0+incompatible + github.com/go-kivik/kivik v2.0.0+incompatible + github.com/go-kivik/kiviktest v2.0.0+incompatible // indirect + github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.4.2 + github.com/stretchr/testify v1.4.0 + gitlab.com/flimzy/testy v0.0.2 // indirect + golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect +) diff --git a/go.sum b/go.sum index 8fdee58..d7cd572 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,50 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/flimzy/diff v0.1.6 h1:ufTsTKcDtlaczpJTo3u1NeYqzuP6oRpy1VwQUIrgmBY= +github.com/flimzy/diff v0.1.6/go.mod h1:lFJtC7SPsK0EroDmGTSrdtWKAxOk3rO+q+e04LL05Hs= +github.com/flimzy/testy v0.1.16 h1:nchF7XYCkfHJiZKMRhAVKQp8jzpXFPwJYnSrnFysqlI= +github.com/flimzy/testy v0.1.16/go.mod h1:3szguN8NXqgq9bt9Gu8TQVj698PJWmyx/VY1frwwKrM= +github.com/go-kivik/couchdb v2.0.0+incompatible h1:DsXVuGJTng04Guz8tg7jGVQ53RlByEhk+gPB/1yo3Oo= +github.com/go-kivik/couchdb v2.0.0+incompatible/go.mod h1:5XJRkAMpBlEVA4q0ktIZjUPYBjoBmRoiWvwUBzP3BOQ= +github.com/go-kivik/kivik v2.0.0+incompatible h1:/7hgr29DKv/vlaJsUoyRlOFq0K+3ikz0wTbu+cIs7QY= +github.com/go-kivik/kivik v2.0.0+incompatible/go.mod h1:nIuJ8z4ikBrVUSk3Ua8NoDqYKULPNjuddjqRvlSUyyQ= +github.com/go-kivik/kiviktest v2.0.0+incompatible h1:y1RyPHqWQr+eFlevD30Tr3ipiPCxK78vRoD3o9YysjI= +github.com/go-kivik/kiviktest v2.0.0+incompatible/go.mod h1:JdhVyzixoYhoIDUt6hRf1yAfYyaDa5/u9SDOindDkfQ= +github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d h1:vr95xIx8Eg3vCzZPxY3rCwTfkjqNDt/FgVqTOk0WByk= +github.com/gopherjs/gopherjs v0.0.0-20200209183636-89e6cbcd0b6d/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/otiai10/copy v1.0.2 h1:DDNipYy6RkIkjMwy+AWzgKiNTyj2RUI9yEMeETEpVyc= +github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95 h1:+OLn68pqasWca0z5ryit9KGfp3sUsW4Lqg32iRMJyzs= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gitlab.com/flimzy/testy v0.0.2 h1:wii65HpZEbstqGT44+msiwzrX7SaxqNXj0BFjJc9iUY= +gitlab.com/flimzy/testy v0.0.2/go.mod h1:YObF4cq711ubd/3U0ydRQQVz7Cnq/ChgJpVwNr/AJac= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/pkg/storage/couchdb/couchdbstore.go b/pkg/storage/couchdb/couchdbstore.go new file mode 100644 index 0000000..d16d29d --- /dev/null +++ b/pkg/storage/couchdb/couchdbstore.go @@ -0,0 +1,204 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package couchdbstore + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "sync" + + _ "github.com/go-kivik/couchdb" // The CouchDB driver + "github.com/go-kivik/kivik" + + "github.com/trustbloc/edge-core/pkg/storage" +) + +// Provider represents an CouchDB implementation of the storage.Provider interface +type Provider struct { + hostURL string + couchDBClient *kivik.Client + dbs map[string]*CouchDBStore + mux sync.RWMutex +} + +const ( + blankHostErrMsg = "hostURL for new CouchDB provider can't be blank" + failToCloseProviderErrMsg = "failed to close provider" +) + +// NewProvider instantiates Provider +func NewProvider(hostURL string) (*Provider, error) { + if hostURL == "" { + return nil, errors.New(blankHostErrMsg) + } + + client, err := kivik.New("couch", hostURL) + if err != nil { + return nil, err + } + + return &Provider{hostURL: hostURL, couchDBClient: client, dbs: map[string]*CouchDBStore{}}, nil +} + +// CreateStore creates a new store with the given name. +func (p *Provider) CreateStore(name string) error { + p.mux.Lock() + defer p.mux.Unlock() + + err := p.couchDBClient.CreateDB(context.Background(), name) + + return err +} + +// OpenStore opens an existing store with the given name and returns it. +func (p *Provider) OpenStore(name string) (storage.Store, error) { + p.mux.Lock() + defer p.mux.Unlock() + + // Check cache first + cachedStore, existsInCache := p.dbs[name] + if existsInCache { + return cachedStore, nil + } + + // If it's not in the cache, then let's ask the CouchDB server if it exists + existsOnServer, err := p.couchDBClient.DBExists(context.Background(), name) + if err != nil { + return nil, err + } + + if !existsOnServer { + return nil, storage.ErrStoreNotFound + } + + db := p.couchDBClient.DB(context.Background(), name) + + // db.Err() won't return an error if the database doesn't exist, hence the need for the explicit DBExists call above + if dbErr := db.Err(); dbErr != nil { + return nil, dbErr + } + + store := &CouchDBStore{db: db} + + p.dbs[name] = store + + return store, nil +} + +// CloseStore closes a previously opened store. +func (p *Provider) CloseStore(name string) error { + p.mux.Lock() + defer p.mux.Unlock() + + store, exists := p.dbs[name] + if !exists { + return storage.ErrStoreNotFound + } + + delete(p.dbs, name) + + return store.db.Close(context.Background()) +} + +// Close closes the provider. +func (p *Provider) Close() error { + p.mux.Lock() + defer p.mux.Unlock() + + for _, store := range p.dbs { + err := store.db.Close(context.Background()) + if err != nil { + return fmt.Errorf(failToCloseProviderErrMsg+": %w", err) + } + } + + return p.couchDBClient.Close(context.Background()) +} + +// CouchDBStore represents a CouchDB-backed database. +type CouchDBStore struct { + db *kivik.DB +} + +// Put stores the given key-value pair in the store. +func (c *CouchDBStore) Put(k string, v []byte) error { + var valueToPut []byte + if isJSON(v) { + valueToPut = v + } else { + valueToPut = wrapTextAsCouchDBAttachment(v) + } + + _, err := c.db.Put(context.Background(), k, valueToPut) + if err != nil { + return err + } + + return nil +} + +func isJSON(textToCheck []byte) bool { + var js json.RawMessage + return json.Unmarshal(textToCheck, &js) == nil +} + +// Kivik has a PutAttachment method, but it requires creating a document first and then adding an attachment after. +// We want to do it all in one step, hence this manual stuff below. +func wrapTextAsCouchDBAttachment(textToWrap []byte) []byte { + encodedTextToWrap := base64.StdEncoding.EncodeToString(textToWrap) + return []byte(`{"_attachments": {"data": {"data": "` + encodedTextToWrap + `", "content_type": "text/plain"}}}`) +} + +// Get retrieves the value in the store associated with the given key. +func (c *CouchDBStore) Get(k string) ([]byte, error) { + destinationData := make(map[string]interface{}) + + row := c.db.Get(context.Background(), k) + + err := row.ScanDoc(&destinationData) + if err != nil { + if err.Error() == "Not Found: missing" { + return nil, storage.ErrValueNotFound + } + + return nil, err + } + + _, containsAttachment := destinationData["_attachments"] + if containsAttachment { + return c.getDataFromAttachment(k) + } + + // Stripping out the CouchDB-specific fields + delete(destinationData, "_id") + delete(destinationData, "_rev") + + strippedJSON, err := json.Marshal(destinationData) + if err != nil { + return nil, err + } + + return strippedJSON, nil +} + +func (c *CouchDBStore) getDataFromAttachment(k string) ([]byte, error) { + // Original data was not JSON and so it was stored as an attachment + attachment, err := c.db.GetAttachment(context.Background(), k, "data") + if err != nil { + return nil, err + } + + data, err := ioutil.ReadAll(attachment.Content) + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/pkg/storage/couchdb/couchdbstore_test.go b/pkg/storage/couchdb/couchdbstore_test.go new file mode 100644 index 0000000..43ae305 --- /dev/null +++ b/pkg/storage/couchdb/couchdbstore_test.go @@ -0,0 +1,292 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package couchdbstore + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/trustbloc/edge-core/pkg/storage" + + "github.com/go-kivik/kivik" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +const ( + couchDBURL = "localhost:5984" + testStoreName = "teststore" + testDocKey = "sampleDBKey" + testJSONValue = `{"JSONKey":"JSONValue"}` + testNonJSONValue = "Some arbitrary data" +) + +// For these unit tests to run, you must ensure you have a CouchDB instance running at the URL specified in couchDBURL. +// 'make unit-test' from the terminal will take care of this for you. +// To run the tests manually, start an instance by running docker run -p 5984:5984 couchdb:2.3.1 from a terminal. + +func TestMain(m *testing.M) { + err := waitForCouchDBToStart() + if err != nil { + logrus.Errorf(err.Error() + + ". Make sure you start a couchDB instance using" + + " 'docker run -p 5984:5984 couchdb:2.3.1' before running the unit tests") + os.Exit(1) + } + + os.Exit(m.Run()) +} + +func waitForCouchDBToStart() error { + client, err := kivik.New("couch", couchDBURL) + if err != nil { + return err + } + + timeout := time.After(5 * time.Second) + + for { + select { + case <-timeout: + return fmt.Errorf("timeout: couldn't reach CouchDB server") + default: + _, err = client.AllDBs(context.Background()) + if err == nil { + return nil + } + } + } +} + +func TestNewProvider(t *testing.T) { + t.Run("Valid URL provided", func(t *testing.T) { + provider, err := NewProvider(couchDBURL) + require.NoError(t, err) + require.NotNil(t, provider) + }) + t.Run("Blank URL provided", func(t *testing.T) { + provider, err := NewProvider("") + require.Equal(t, blankHostErrMsg, err.Error()) + require.Nil(t, provider) + }) + + t.Run("Unreachable URL provided", func(t *testing.T) { + provider, err := NewProvider("%") + require.Equal(t, `parse http://%: invalid URL escape "%"`, err.Error()) + require.Nil(t, provider) + }) +} + +func TestProvider_CreateStore(t *testing.T) { + t.Run("Successfully create a new store", func(t *testing.T) { + provider := initializeTest(t) + + err := provider.CreateStore(testStoreName) + require.NoError(t, err) + }) + t.Run("Attempt to create a store that already exists", func(t *testing.T) { + provider := initializeTest(t) + + err := provider.CreateStore(testStoreName) + require.NoError(t, err) + + err = provider.CreateStore(testStoreName) + require.Equal(t, "Precondition Failed: The database could not be created, the file already exists.", + err.Error()) + }) + t.Run("Attempt to create a store with an incompatible name", func(t *testing.T) { + provider := initializeTest(t) + + err := provider.CreateStore("BadName") + require.Equal(t, "Bad Request: Name: 'BadName'. Only lowercase characters (a-z), digits (0-9),"+ + " and any of the characters _, $, (, ), +, -, and / are allowed. Must begin with a letter.", err.Error()) + }) +} + +func TestProvider_OpenStore(t *testing.T) { + t.Run("Successfully open an existing store - already in cache", func(t *testing.T) { + provider := initializeTest(t) + + newStore := createAndOpenTestStore(t, provider) + require.IsType(t, &CouchDBStore{}, newStore) + + // The OpenStore call will cache the store in the provider. + store, err := provider.OpenStore(testStoreName) + require.NoError(t, err) + + require.Equal(t, 1, len(provider.dbs)) + require.NotNil(t, store) + require.Equal(t, newStore, store) + }) + t.Run("Successfully open an existing store - not in cache", func(t *testing.T) { + provider := initializeTest(t) + + newStore := createAndOpenTestStore(t, provider) + require.IsType(t, &CouchDBStore{}, newStore) + + require.Equal(t, 1, len(provider.dbs)) + store := provider.dbs[testStoreName] + require.NotNil(t, store) + require.Equal(t, newStore, store) + }) + t.Run("Attempt to open a non-existent store", func(t *testing.T) { + provider := initializeTest(t) + + newStore, err := provider.OpenStore(testStoreName) + require.Nil(t, newStore) + require.Equal(t, storage.ErrStoreNotFound, err) + }) + t.Run("Attempt to open a store with a blank name", func(t *testing.T) { + provider := initializeTest(t) + + newStore, err := provider.OpenStore("") + require.Nil(t, newStore) + require.Equal(t, "kivik: dbName required", err.Error()) + }) +} + +func TestProvider_CloseStore(t *testing.T) { + t.Run("Successfully close a store", func(t *testing.T) { + provider := initializeTest(t) + + _ = createAndOpenTestStore(t, provider) + + err := provider.CloseStore(testStoreName) + require.NoError(t, err) + }) + t.Run("Attempt to close a non-existent store", func(t *testing.T) { + provider := initializeTest(t) + + err := provider.CloseStore(testStoreName) + require.Equal(t, storage.ErrStoreNotFound, err) + }) +} + +func TestProvider_Close(t *testing.T) { + provider := initializeTest(t) + + _ = createAndOpenTestStore(t, provider) + + err := provider.Close() + require.NoError(t, err) +} + +func TestCouchDBStore_Put(t *testing.T) { + t.Run("Value is JSON", func(t *testing.T) { + provider := initializeTest(t) + + store := createAndOpenTestStore(t, provider) + + err := store.Put(testDocKey, []byte(testJSONValue)) + require.NoError(t, err) + }) + + t.Run("Value is not JSON", func(t *testing.T) { + provider := initializeTest(t) + + store := createAndOpenTestStore(t, provider) + + err := store.Put(testDocKey, []byte(testNonJSONValue)) + require.NoError(t, err) + }) +} + +func TestCouchDBStore_Get(t *testing.T) { + t.Run("Document found, original data was JSON and is preserved as such", func(t *testing.T) { + provider := initializeTest(t) + + store := createAndOpenTestStore(t, provider) + + err := store.Put(testDocKey, []byte(testJSONValue)) + require.NoError(t, err) + + value, err := store.Get(testDocKey) + require.NoError(t, err) + require.Equal(t, testJSONValue, string(value)) + }) + t.Run("Document found, original data was not JSON and so was saved as a CouchDB attachment."+ + " Original data is still preserved", func(t *testing.T) { + provider := initializeTest(t) + + store := createAndOpenTestStore(t, provider) + + err := store.Put(testDocKey, []byte(testNonJSONValue)) + require.NoError(t, err) + + value, err := store.Get(testDocKey) + require.NoError(t, err) + require.Equal(t, testNonJSONValue, string(value)) + }) + t.Run("Document not found", func(t *testing.T) { + provider := initializeTest(t) + + store := createAndOpenTestStore(t, provider) + + value, err := store.Get(testDocKey) + require.Nil(t, value) + require.Equal(t, storage.ErrValueNotFound.Error(), err.Error()) + }) +} + +func TestCouchDBStore_getDataFromAttachment(t *testing.T) { + t.Run("Attachment found", func(t *testing.T) { + provider := initializeTest(t) + + _ = createAndOpenTestStore(t, provider) + + _, err := provider.dbs[testStoreName].db.Put(context.Background(), testDocKey, + wrapTextAsCouchDBAttachment([]byte(testNonJSONValue))) + require.NoError(t, err) + + data, err := provider.dbs[testStoreName].getDataFromAttachment(testDocKey) + require.NoError(t, err) + require.Equal(t, testNonJSONValue, string(data)) + }) + t.Run("Attachment not found", func(t *testing.T) { + provider := initializeTest(t) + + _ = createAndOpenTestStore(t, provider) + + _, err := provider.dbs[testStoreName].db.Put(context.Background(), testDocKey, []byte(testJSONValue)) + require.NoError(t, err) + + data, err := provider.dbs[testStoreName].getDataFromAttachment(testDocKey) + require.Nil(t, data) + require.Equal(t, "Not Found: Document is missing attachment", err.Error()) + }) +} + +func initializeTest(t *testing.T) *Provider { + provider, err := NewProvider(couchDBURL) + require.NoError(t, err) + + resetCouchDB(t, provider) + + return provider +} + +// Wipes out the test database that may still exist from a previous test. +func resetCouchDB(t *testing.T, p *Provider) { + err := p.couchDBClient.DestroyDB(context.Background(), testStoreName) + + if err != nil { + require.Equal(t, "Not Found: Database does not exist.", err.Error()) + } +} + +func createAndOpenTestStore(t *testing.T, provider *Provider) storage.Store { + err := provider.CreateStore(testStoreName) + require.NoError(t, err) + + newStore, err := provider.OpenStore(testStoreName) + require.NotNil(t, newStore) + require.NoError(t, err) + + return newStore +} diff --git a/pkg/storage/memstore/memstore.go b/pkg/storage/memstore/memstore.go index 4951dfd..b737bfa 100644 --- a/pkg/storage/memstore/memstore.go +++ b/pkg/storage/memstore/memstore.go @@ -6,7 +6,9 @@ SPDX-License-Identifier: Apache-2.0 package memstore -import "github.com/trustbloc/edge-core/pkg/storage" +import ( + "github.com/trustbloc/edge-core/pkg/storage" +) // Provider represents an MemStore implementation of the storage.Provider interface type Provider struct { @@ -18,24 +20,25 @@ func NewProvider() *Provider { return &Provider{dbs: make(map[string]*MemStore)} } -// OpenStore opens and returns a store for the given name. +// CreateStore creates a new store with the given name. +func (p *Provider) CreateStore(name string) error { + store := MemStore{db: make(map[string][]byte)} + + p.dbs[name] = &store + + return nil +} + +// OpenStore opens an existing store with the given name and returns it. func (p *Provider) OpenStore(name string) (storage.Store, error) { store, exists := p.dbs[name] if !exists { - return p.newMemStore(name), nil + return nil, storage.ErrStoreNotFound } return store, nil } -func (p *Provider) newMemStore(name string) *MemStore { - store := MemStore{db: make(map[string][]byte)} - - p.dbs[name] = &store - - return &store -} - // CloseStore closes a previously opened store. func (p *Provider) CloseStore(name string) error { store, exists := p.dbs[name] @@ -67,15 +70,15 @@ type MemStore struct { } // Put stores the given key-value pair in the store. -func (store *MemStore) Put(k string, v []byte) error { - store.db[k] = v +func (m *MemStore) Put(k string, v []byte) error { + m.db[k] = v return nil } // Get retrieves the value in the store associated with the given key. -func (store *MemStore) Get(k string) ([]byte, error) { - v, exists := store.db[k] +func (m *MemStore) Get(k string) ([]byte, error) { + v, exists := m.db[k] if !exists { return nil, storage.ErrValueNotFound } @@ -83,6 +86,6 @@ func (store *MemStore) Get(k string) ([]byte, error) { return v, nil } -func (store *MemStore) close() { - store.db = make(map[string][]byte) +func (m *MemStore) close() { + m.db = make(map[string][]byte) } diff --git a/pkg/storage/memstore/memstore_test.go b/pkg/storage/memstore/memstore_test.go index 4ce10c4..9e38c85 100644 --- a/pkg/storage/memstore/memstore_test.go +++ b/pkg/storage/memstore/memstore_test.go @@ -14,75 +14,108 @@ import ( "github.com/stretchr/testify/require" ) -func TestMemStore_OpenStore(t *testing.T) { +const testStoreName = "teststore" + +func TestProvider_CreateStore(t *testing.T) { provider := NewProvider() - newStore, err := provider.OpenStore("store1") + err := provider.CreateStore(testStoreName) require.NoError(t, err) - require.IsType(t, &MemStore{}, newStore) } -func TestMemStore_OpenExistingStore(t *testing.T) { - provider := NewProvider() +func TestMemStore_OpenStore(t *testing.T) { + t.Run("Successfully open an existing store", func(t *testing.T) { + provider := NewProvider() + + err := provider.CreateStore(testStoreName) + require.NoError(t, err) + + newStore, err := provider.OpenStore(testStoreName) + require.NoError(t, err) + require.IsType(t, &MemStore{}, newStore) + }) + t.Run("Attempt to open a non-existent store", func(t *testing.T) { + provider := NewProvider() + + newStore, err := provider.OpenStore(testStoreName) + require.Nil(t, newStore) + require.Equal(t, storage.ErrStoreNotFound, err) + }) +} - newStore, err := provider.OpenStore("store1") - require.NoError(t, err) - require.IsType(t, &MemStore{}, newStore) +func TestProvider_CloseStore(t *testing.T) { + t.Run("Successfully close store", func(t *testing.T) { + provider := NewProvider() - existingStore, err := provider.OpenStore("store1") - require.NoError(t, err) - require.Equal(t, newStore, existingStore) -} + err := provider.CreateStore(testStoreName) + require.NoError(t, err) -func TestProvider_Close(t *testing.T) { - provider := NewProvider() + newStore, err := provider.OpenStore(testStoreName) + require.NoError(t, err) - _, err := provider.OpenStore("store1") - require.NoError(t, err) + err = newStore.Put("something", []byte("value")) + require.NoError(t, err) - _, err = provider.OpenStore("store2") - require.NoError(t, err) + err = provider.CreateStore("store2") + require.NoError(t, err) - err = provider.Close() - require.NoError(t, err) + _, err = provider.OpenStore("store2") + require.NoError(t, err) - require.Equal(t, 0, len(provider.dbs)) + err = provider.CloseStore(testStoreName) + require.NoError(t, err) + + _, err = newStore.Get("something") + require.Equal(t, storage.ErrValueNotFound, err) + + require.Equal(t, 1, len(provider.dbs)) + }) + t.Run("Attempt to close a non-existent store", func(t *testing.T) { + provider := NewProvider() + + err := provider.CloseStore(testStoreName) + require.Equal(t, storage.ErrStoreNotFound, err) + }) } -func TestProvider_CloseStore(t *testing.T) { +func TestProvider_Close(t *testing.T) { provider := NewProvider() - newStore, err := provider.OpenStore("store1") + err := provider.CreateStore(testStoreName) require.NoError(t, err) - err = newStore.Put("something", []byte("value")) + _, err = provider.OpenStore(testStoreName) require.NoError(t, err) - _, err = provider.OpenStore("store2") + err = provider.CreateStore("store2") require.NoError(t, err) - err = provider.CloseStore("store1") + _, err = provider.OpenStore("store2") require.NoError(t, err) - _, err = newStore.Get("something") - require.Equal(t, storage.ErrValueNotFound, err) + err = provider.Close() + require.NoError(t, err) - require.Equal(t, 1, len(provider.dbs)) + require.Equal(t, 0, len(provider.dbs)) } -func TestProvider_CloseStoreDoesNotExist(t *testing.T) { - provider := NewProvider() +func TestMemStore_Put(t *testing.T) { + store := MemStore{db: map[string][]byte{}} + + err := store.Put("someKey", []byte("someValue")) + require.NoError(t, err) - err := provider.CloseStore("store1") - require.Equal(t, storage.ErrStoreNotFound, err) + value, exists := store.db["someKey"] + require.True(t, exists) + require.Equal(t, "someValue", string(value)) } func TestMemStore_Get(t *testing.T) { - memStore := MemStore{db: make(map[string][]byte)} + store := MemStore{db: make(map[string][]byte)} - memStore.db["testKey"] = []byte("testValue") + store.db["testKey"] = []byte("testValue") - value, err := memStore.Get("testKey") + value, err := store.Get("testKey") require.NoError(t, err) require.Equal(t, []byte("testValue"), value) diff --git a/pkg/storage/store.go b/pkg/storage/store.go index 37b6759..7df91a5 100644 --- a/pkg/storage/store.go +++ b/pkg/storage/store.go @@ -16,7 +16,10 @@ var ErrValueNotFound = errors.New("store does not have a value associated with t // Provider represents a storage provider. type Provider interface { - // OpenStore opens a store with the given name and returns it. + // CreateStore creates a new store with the given name. + CreateStore(name string) error + + // OpenStore opens an existing store and returns it. OpenStore(name string) (Store, error) // CloseStore closes the store with the given name. diff --git a/scripts/check_unit.sh b/scripts/check_unit.sh index ad03437..3fcad3a 100755 --- a/scripts/check_unit.sh +++ b/scripts/check_unit.sh @@ -19,8 +19,53 @@ if [ -f profile.out ]; then fi } +# First argument is the exit code +# Second argument is the command that was run +check_exit_code () { +if [ "$1" -ne 0 ] && [ "$1" -ne 1 ]; then + echo "error: '${2}' returned ${1}, but either 0 or 1 was expected." + + # There's no easy way to print the error message on the screen without temporary files, + # so we ask the user to check manually + echo "Try running '${2}' manually to see the full error message." + + exit 1 +fi +} + +# docker rm returns 1 if the image isn't found. This is OK and expected, so we suppress it +# Any return status other than 0 or 1 is unusual and so we exit +remove_docker_container () { +DOCKER_KILL_EXIT_CODE=0 +docker kill CouchDBStoreTest >/dev/null 2>&1 || DOCKER_KILL_EXIT_CODE=$? + +check_exit_code $DOCKER_KILL_EXIT_CODE "docker kill CouchDBStoreTest" + +DOCKER_RM_EXIT_CODE=0 +docker rm CouchDBStoreTest >/dev/null 2>&1 || DOCKER_RM_EXIT_CODE=$? + +check_exit_code $DOCKER_RM_EXIT_CODE "docker rm CouchDBStoreTest" +} + +remove_docker_container + PKGS=`go list github.com/trustbloc/edge-core/... 2> /dev/null | \ grep -v /mocks` -go test $PKGS -count=1 -race -coverprofile=profile.out -covermode=atomic -timeout=10m + +docker run -p 5984:5984 -d --name CouchDBStoreTest couchdb:2.3.1 >/dev/null + +GO_TEST_EXIT_CODE=0 +go test $PKGS -count=1 -race -coverprofile=profile.out -covermode=atomic -timeout=10m || GO_TEST_EXIT_CODE=$? +if [ $GO_TEST_EXIT_CODE -ne 0 ]; then + docker kill CouchDBStoreTest >/dev/null + remove_docker_container + + exit $GO_TEST_EXIT_CODE +fi + amend_coverage_file + +docker kill CouchDBStoreTest >/dev/null +remove_docker_container + cd "$pwd" || exit