Add config file authentication to oci object storage state store (#1430)
* Improving testing - verify expected methods are invoked Signed-off-by: lucasjellema <lucasjellema@gmail.com> * Adding support for composite keys (mapped to directories in buckets) Signed-off-by: lucasjellema <lucasjellema@gmail.com> * Adding support for TTL - expired elements are not returned in Get Signed-off-by: lucasjellema <lucasjellema@gmail.com> * fixing linting issues Signed-off-by: lucasjellema <lucasjellema@gmail.com> * fixing linting issue Signed-off-by: lucasjellema <lucasjellema@gmail.com> * adding support for TTL == -1 (never expire) Signed-off-by: lucasjellema <lucasjellema@gmail.com> * fix linting issue Signed-off-by: lucasjellema <lucasjellema@gmail.com> * introducing Instance Principal authentication for OCI client Signed-off-by: lucasjellema <lucasjellema@gmail.com> * fix linting issue Signed-off-by: lucasjellema <lucasjellema@gmail.com> * adding support for oci config file based authentication for OCI bjectStorage state store Signed-off-by: lucasjellema <lucasjellema@gmail.com> * expand ~ to user home directory in config file path; fix linting findings Signed-off-by: lucasjellema <lucasjellema@gmail.com> * gofumpt file because of linting issue Signed-off-by: lucasjellema <lucasjellema@gmail.com> * Do not allow config file path to start with ~/ Signed-off-by: lucasjellema <lucasjellema@gmail.com> * introducing integration test for oci objectstorage state store Signed-off-by: lucasjellema <lucasjellema@gmail.com> * Fixing linting issues in integration test Signed-off-by: lucasjellema <lucasjellema@gmail.com> * Fixing linting issues in integration test Signed-off-by: lucasjellema <lucasjellema@gmail.com> Co-authored-by: Looong Dai <long.dai@intel.com> Co-authored-by: Yaron Schneider <schneider.yaron@live.com>
This commit is contained in:
parent
4885a835fc
commit
a383697ef5
|
|
@ -1,7 +1,15 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
/*
|
||||
Copyright 2021 The Dapr Authors
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package objectstorage
|
||||
|
||||
|
|
@ -11,11 +19,16 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v54/common"
|
||||
"github.com/oracle/oci-go-sdk/v54/common/auth"
|
||||
"github.com/oracle/oci-go-sdk/v54/objectstorage"
|
||||
|
||||
"github.com/dapr/components-contrib/state"
|
||||
|
|
@ -23,14 +36,22 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
keyDelimiter = "||"
|
||||
tenancyKey = "tenancyOCID"
|
||||
compartmentKey = "compartmentOCID"
|
||||
regionKey = "region"
|
||||
fingerPrintKey = "fingerPrint"
|
||||
privateKeyKey = "privateKey"
|
||||
userKey = "userOCID"
|
||||
bucketNameKey = "bucketName"
|
||||
keyDelimiter = "||"
|
||||
instancePrincipalAuthenticationKey = "instancePrincipalAuthentication"
|
||||
configFileAuthenticationKey = "configFileAuthentication"
|
||||
configFilePathKey = "configFilePath"
|
||||
configFileProfileKey = "configFileProfile"
|
||||
tenancyKey = "tenancyOCID"
|
||||
compartmentKey = "compartmentOCID"
|
||||
regionKey = "region"
|
||||
fingerPrintKey = "fingerPrint"
|
||||
privateKeyKey = "privateKey"
|
||||
userKey = "userOCID"
|
||||
bucketNameKey = "bucketName"
|
||||
metadataTTLKey = "ttlInSeconds"
|
||||
daprStateStoreMetaLabel = "dapr-state-store"
|
||||
expiryTimeMetaLabel = "expiry-time-from-ttl"
|
||||
isoDateTimeFormat = "2006-01-02T15:04:05"
|
||||
)
|
||||
|
||||
type StateStore struct {
|
||||
|
|
@ -43,19 +64,24 @@ type StateStore struct {
|
|||
}
|
||||
|
||||
type Metadata struct {
|
||||
userOCID string
|
||||
bucketName string
|
||||
region string
|
||||
tenancyOCID string
|
||||
fingerPrint string
|
||||
privateKey string
|
||||
compartmentOCID string
|
||||
namespace string
|
||||
userOCID string
|
||||
bucketName string
|
||||
region string
|
||||
tenancyOCID string
|
||||
fingerPrint string
|
||||
privateKey string
|
||||
compartmentOCID string
|
||||
namespace string
|
||||
configFilePath string
|
||||
configFileProfile string
|
||||
instancePrincipalAuthentication bool
|
||||
configFileAuthentication bool
|
||||
|
||||
OCIObjectStorageClient *objectstorage.ObjectStorageClient
|
||||
}
|
||||
|
||||
type objectStoreClient interface {
|
||||
getObject(ctx context.Context, objectname string, logger logger.Logger) ([]byte, *string, error)
|
||||
getObject(ctx context.Context, objectname string, logger logger.Logger) (content []byte, etag *string, metadata map[string]string, err error)
|
||||
deleteObject(ctx context.Context, objectname string, etag *string) (err error)
|
||||
putObject(ctx context.Context, objectname string, contentLen int64, content io.ReadCloser, metadata map[string]string, etag *string, logger logger.Logger) error
|
||||
initStorageBucket(logger logger.Logger) error
|
||||
|
|
@ -147,38 +173,92 @@ func NewOCIObjectStorageStore(logger logger.Logger) *StateStore {
|
|||
|
||||
/************** private helper functions. */
|
||||
|
||||
func getValue(metadata map[string]string, key string, valueRequired bool) (value string, err error) {
|
||||
if val, ok := metadata[key]; ok && val != "" {
|
||||
return val, nil
|
||||
}
|
||||
if !valueRequired {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("missing or empty %s field from metadata", key)
|
||||
}
|
||||
|
||||
func getOptionalBooleanValue(metadata map[string]string, key string) (value bool, err error) {
|
||||
stringValue, _ := getValue(metadata, key, false)
|
||||
if stringValue == "" {
|
||||
stringValue = "false"
|
||||
}
|
||||
value, err = strconv.ParseBool(stringValue)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("incorrect value %s for %s, should be 'true' or 'false'", stringValue, key)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func getConfigFilePath(meta map[string]string) (value string, err error) {
|
||||
value, _ = getValue(meta, configFilePathKey, false)
|
||||
if strings.HasPrefix(value, "~/") {
|
||||
return "", fmt.Errorf("%s is set to %s which starts with ~/; this is not supported - please provide absolute path to configuration file", configFilePathKey, value)
|
||||
}
|
||||
if value != "" {
|
||||
if _, err = os.Stat(value); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("oci configuration file %s does not exist %w", value, err)
|
||||
}
|
||||
return "", fmt.Errorf("error %w with reading oci configuration file %s", err, value)
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func getObjectStorageMetadata(metadata map[string]string) (*Metadata, error) {
|
||||
meta := Metadata{}
|
||||
var err error
|
||||
if meta.bucketName, err = getValue(metadata, bucketNameKey); err != nil {
|
||||
if meta.instancePrincipalAuthentication, err = getOptionalBooleanValue(metadata, instancePrincipalAuthenticationKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta.region, err = getValue(metadata, regionKey); err != nil {
|
||||
if meta.configFileAuthentication, err = getOptionalBooleanValue(metadata, configFileAuthenticationKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta.compartmentOCID, err = getValue(metadata, compartmentKey); err != nil {
|
||||
if meta.configFileAuthentication {
|
||||
if meta.configFilePath, err = getConfigFilePath(metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta.configFileProfile, _ = getValue(metadata, configFileProfileKey, false)
|
||||
}
|
||||
if meta.bucketName, err = getValue(metadata, bucketNameKey, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta.userOCID, err = getValue(metadata, userKey); err != nil {
|
||||
if meta.compartmentOCID, err = getValue(metadata, compartmentKey, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta.fingerPrint, err = getValue(metadata, fingerPrintKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta.privateKey, err = getValue(metadata, privateKeyKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta.tenancyOCID, err = getValue(metadata, tenancyKey); err != nil {
|
||||
return nil, err
|
||||
externalAuthentication := meta.instancePrincipalAuthentication || meta.configFileAuthentication
|
||||
if !externalAuthentication {
|
||||
err = getIdentityAuthenticationDetails(metadata, &meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func getValue(metadata map[string]string, key string) (string, error) {
|
||||
if val, ok := metadata[key]; ok && val != "" {
|
||||
return val, nil
|
||||
func getIdentityAuthenticationDetails(metadata map[string]string, meta *Metadata) (err error) {
|
||||
if meta.region, err = getValue(metadata, regionKey, true); err != nil {
|
||||
return err
|
||||
}
|
||||
return "", fmt.Errorf("missing or empty %s field from metadata", key)
|
||||
if meta.userOCID, err = getValue(metadata, userKey, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if meta.fingerPrint, err = getValue(metadata, fingerPrintKey, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if meta.privateKey, err = getValue(metadata, privateKeyKey, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if meta.tenancyOCID, err = getValue(metadata, tenancyKey, true); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// functions that bridge from the Dapr State API to the OCI ObjectStorage Client.
|
||||
|
|
@ -190,6 +270,12 @@ func (r *StateStore) writeDocument(req *state.SetRequest) error {
|
|||
r.logger.Debugf("when FirstWrite is to be enforced, a value must be provided for the ETag")
|
||||
return fmt.Errorf("when FirstWrite is to be enforced, a value must be provided for the ETag")
|
||||
}
|
||||
metadata := (map[string]string{"category": daprStateStoreMetaLabel})
|
||||
|
||||
err := convertTTLtoExpiryTime(req, r.logger, metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process ttl meta data: %w", err)
|
||||
}
|
||||
|
||||
r.logger.Debugf("Save state in OCI Object Storage Bucket under key %s ", req.Key)
|
||||
objectName := getFileName(req.Key)
|
||||
|
|
@ -200,7 +286,7 @@ func (r *StateStore) writeDocument(req *state.SetRequest) error {
|
|||
if req.Options.Concurrency != state.FirstWrite {
|
||||
etag = nil
|
||||
}
|
||||
err := r.client.putObject(ctx, objectName, objectLength, ioutil.NopCloser(bytes.NewReader(content)), nil, etag, r.logger)
|
||||
err = r.client.putObject(ctx, objectName, objectLength, ioutil.NopCloser(bytes.NewReader(content)), metadata, etag, r.logger)
|
||||
if err != nil {
|
||||
r.logger.Debugf("error in writing object to OCI object storage %s, err %s", req.Key, err)
|
||||
return fmt.Errorf("failed to write object to OCI Object storage : %w", err)
|
||||
|
|
@ -208,17 +294,43 @@ func (r *StateStore) writeDocument(req *state.SetRequest) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func convertTTLtoExpiryTime(req *state.SetRequest, logger logger.Logger, metadata map[string]string) error {
|
||||
ttl, ttlerr := parseTTL(req.Metadata)
|
||||
if ttlerr != nil {
|
||||
return fmt.Errorf("error in parsing TTL %w", ttlerr)
|
||||
}
|
||||
if ttl != nil {
|
||||
if *ttl == -1 {
|
||||
logger.Debugf("TTL is set to -1; this means: never expire. ")
|
||||
} else {
|
||||
metadata[expiryTimeMetaLabel] = time.Now().UTC().Add(time.Second * time.Duration(*ttl)).Format(isoDateTimeFormat)
|
||||
logger.Debugf("Set %s in meta properties for object to ", expiryTimeMetaLabel, metadata[expiryTimeMetaLabel])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *StateStore) readDocument(req *state.GetRequest) ([]byte, *string, error) {
|
||||
if len(req.Key) == 0 || req.Key == "" {
|
||||
return nil, nil, fmt.Errorf("key for value to get was missing from request")
|
||||
}
|
||||
objectName := getFileName(req.Key)
|
||||
ctx := context.Background()
|
||||
content, etag, err := r.client.getObject(ctx, objectName, r.logger)
|
||||
content, etag, meta, err := r.client.getObject(ctx, objectName, r.logger)
|
||||
if err != nil {
|
||||
r.logger.Debugf("download file %s, err %s", req.Key, err)
|
||||
return nil, nil, fmt.Errorf("failed to read object from OCI Object storage : %w", err)
|
||||
}
|
||||
if expiryTimeString, ok := meta[expiryTimeMetaLabel]; ok {
|
||||
expirationTime, err := time.Parse(isoDateTimeFormat, expiryTimeString)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get object from OCI because of invalid formatted value %s in meta property %s : %w", expiryTimeString, expiryTimeMetaLabel, err)
|
||||
}
|
||||
if time.Now().UTC().After(expirationTime) {
|
||||
r.logger.Debug("failed to get object from OCI because it has expired; expiry time set to %s", expiryTimeString)
|
||||
return nil, nil, nil
|
||||
}
|
||||
}
|
||||
return content, etag, nil
|
||||
}
|
||||
|
||||
|
|
@ -271,7 +383,21 @@ func getFileName(key string) string {
|
|||
return pr[0]
|
||||
}
|
||||
|
||||
return pr[1]
|
||||
return path.Join(pr[0], pr[1])
|
||||
}
|
||||
|
||||
func parseTTL(requestMetadata map[string]string) (*int, error) {
|
||||
if val, found := requestMetadata[metadataTTLKey]; found && val != "" {
|
||||
parsedVal, err := strconv.ParseInt(val, 10, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error in parsing ttl metadata : %w", err)
|
||||
}
|
||||
parsedInt := int(parsedVal)
|
||||
|
||||
return &parsedInt, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
/**************** functions with OCI ObjectStorage Service interaction. */
|
||||
|
|
@ -326,7 +452,7 @@ func createBucket(ctx context.Context, client objectstorage.ObjectStorageClient,
|
|||
|
||||
// ***** the functions that interact with OCI Object Storage AND constitute the objectStoreClient interface.
|
||||
|
||||
func (c *ociObjectStorageClient) getObject(ctx context.Context, objectname string, logger logger.Logger) ([]byte, *string, error) {
|
||||
func (c *ociObjectStorageClient) getObject(ctx context.Context, objectname string, logger logger.Logger) (content []byte, etag *string, metadata map[string]string, err error) {
|
||||
logger.Debugf("read file %s from OCI ObjectStorage StateStore %s ", objectname, &c.objectStorageMetadata.bucketName)
|
||||
request := objectstorage.GetObjectRequest{
|
||||
NamespaceName: &c.objectStorageMetadata.namespace,
|
||||
|
|
@ -337,16 +463,13 @@ func (c *ociObjectStorageClient) getObject(ctx context.Context, objectname strin
|
|||
if err != nil {
|
||||
logger.Debugf("Issue in OCI ObjectStorage with retrieving object %s, error: %s", objectname, err)
|
||||
if response.RawResponse.StatusCode == 404 {
|
||||
return nil, nil, nil
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to retrieve object : %w", err)
|
||||
}
|
||||
if response.ETag != nil {
|
||||
logger.Debugf("OCI ObjectStorage StateStore metadata: ETag %s", *response.ETag)
|
||||
return nil, nil, nil, fmt.Errorf("failed to retrieve object : %w", err)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(response.Content)
|
||||
return buf.Bytes(), response.ETag, nil
|
||||
return buf.Bytes(), response.ETag, response.OpcMeta, nil
|
||||
}
|
||||
|
||||
func (c *ociObjectStorageClient) deleteObject(ctx context.Context, objectname string, etag *string) (err error) {
|
||||
|
|
@ -391,7 +514,24 @@ func (c *ociObjectStorageClient) initStorageBucket(logger logger.Logger) error {
|
|||
}
|
||||
|
||||
func (c *ociObjectStorageClient) initOCIObjectStorageClient(logger logger.Logger) (*objectstorage.ObjectStorageClient, error) {
|
||||
configurationProvider := common.NewRawConfigurationProvider(c.objectStorageMetadata.tenancyOCID, c.objectStorageMetadata.userOCID, c.objectStorageMetadata.region, c.objectStorageMetadata.fingerPrint, c.objectStorageMetadata.privateKey, nil)
|
||||
var configurationProvider common.ConfigurationProvider
|
||||
if c.objectStorageMetadata.instancePrincipalAuthentication {
|
||||
logger.Debugf("instance principal authentication is used. ")
|
||||
var err error
|
||||
configurationProvider, err = auth.InstancePrincipalConfigurationProvider()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get oci configurationprovider based on instance principal authentication : %w", err)
|
||||
}
|
||||
} else {
|
||||
if c.objectStorageMetadata.configFileAuthentication {
|
||||
logger.Debugf("configuration file based authentication is used with configuration file path %s and configuration profile %s. ", c.objectStorageMetadata.configFilePath, c.objectStorageMetadata.configFileProfile)
|
||||
configurationProvider = common.CustomProfileConfigProvider(c.objectStorageMetadata.configFilePath, c.objectStorageMetadata.configFileProfile)
|
||||
} else {
|
||||
logger.Debugf("identity authentication is used with configuration provided through Dapr component configuration ")
|
||||
configurationProvider = common.NewRawConfigurationProvider(c.objectStorageMetadata.tenancyOCID, c.objectStorageMetadata.userOCID, c.objectStorageMetadata.region, c.objectStorageMetadata.fingerPrint, c.objectStorageMetadata.privateKey, nil)
|
||||
}
|
||||
}
|
||||
|
||||
objectStorageClient, cerr := objectstorage.NewObjectStorageClientWithConfigurationProvider(configurationProvider)
|
||||
if cerr != nil {
|
||||
return nil, fmt.Errorf("failed to create ObjectStorageClient : %w", cerr)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,296 @@
|
|||
package objectstorage
|
||||
|
||||
// run the test for example in ~/dapr-dev/components-contrib
|
||||
// go test -v github.com/dapr/components-contrib/state/oci/objectstorage.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/dapr/components-contrib/state"
|
||||
"github.com/dapr/kit/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
configFilePathEnvKey = "DAPR_TEST_OCI_CONFIG_FILE_PATH"
|
||||
bucketNameEnvKey = "DAPR_TEST_OCI_BUCKET_NAME"
|
||||
compartmentOCIDEnvKey = "DAPR_TEST_OCI_COMPARTMENT_OCID"
|
||||
configFileProfileEnvKey = "DAPR_TEST_OCI_CONFIG_PROFILE"
|
||||
)
|
||||
|
||||
func getConfigFilePathString() string {
|
||||
return os.Getenv(configFilePathEnvKey)
|
||||
}
|
||||
|
||||
func getConfigProfile() string {
|
||||
return os.Getenv(configFileProfileEnvKey)
|
||||
}
|
||||
|
||||
func getCompartmentOCID() string {
|
||||
return os.Getenv(compartmentOCIDEnvKey)
|
||||
}
|
||||
|
||||
func getBucketName() string {
|
||||
return os.Getenv(bucketNameEnvKey)
|
||||
}
|
||||
|
||||
func TestOCIObjectStorageIntegration(t *testing.T) {
|
||||
ociObjectStorageConfiguration := map[string]string{
|
||||
"configFileAuthentication": "true",
|
||||
}
|
||||
configFilePath := getConfigFilePathString()
|
||||
if configFilePath == "" {
|
||||
// first run export DAPR_TEST_OCI_CONFIG_FILE_PATH="/home/app/.oci/config".
|
||||
t.Skipf("OCI ObjectStorage state integration tests skipped. To enable define the configuration file path string using environment variable '%s' (example 'export %s=\"/home/app/.oci/config\")", configFilePathEnvKey, configFilePathEnvKey)
|
||||
}
|
||||
ociObjectStorageConfiguration["configFilePath"] = configFilePath
|
||||
ociObjectStorageConfiguration["configFileProfile"] = getConfigProfile()
|
||||
|
||||
compartmentOCID := getCompartmentOCID()
|
||||
if compartmentOCID == "" {
|
||||
t.Skipf("OCI ObjectStorage state integration tests skipped. To enable define the compartment OCID string using environment variable '%s' (example 'export %s=\"ocid1.compartment.oc1..aaaaaaaacsssekayq4d34nl5h3eqs5e6ak3j5s4jhlws7rr5pxmt3zrq\")", compartmentOCIDEnvKey, compartmentOCIDEnvKey)
|
||||
}
|
||||
ociObjectStorageConfiguration["compartmentOCID"] = compartmentOCID
|
||||
|
||||
bucketName := getBucketName()
|
||||
if bucketName == "" {
|
||||
bucketName = "DAPR_TEST_BUCKET"
|
||||
}
|
||||
ociObjectStorageConfiguration["bucketName"] = bucketName
|
||||
|
||||
t.Run("Test Get", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testGet(t, ociObjectStorageConfiguration)
|
||||
})
|
||||
t.Run("Test Set", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testSet(t, ociObjectStorageConfiguration)
|
||||
})
|
||||
t.Run("Test Delete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testDelete(t, ociObjectStorageConfiguration)
|
||||
})
|
||||
t.Run("Test Ping", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testPing(t, ociObjectStorageConfiguration)
|
||||
})
|
||||
}
|
||||
|
||||
func testGet(t *testing.T, ociProperties map[string]string) {
|
||||
statestore := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
meta := state.Metadata{}
|
||||
meta.Properties = ociProperties
|
||||
|
||||
t.Run("Get an non-existing key", func(t *testing.T) {
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
getResponse, err := statestore.Get(&state.GetRequest{Key: "xyzq"})
|
||||
assert.Equal(t, &state.GetResponse{}, getResponse, "Response must be empty")
|
||||
assert.NoError(t, err, "Non-existing key must not be treated as error")
|
||||
})
|
||||
t.Run("Get an existing key", func(t *testing.T) {
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
err = statestore.Set(&state.SetRequest{Key: "test-key", Value: []byte("test-value")})
|
||||
assert.Nil(t, err)
|
||||
getResponse, err := statestore.Get(&state.GetRequest{Key: "test-key"})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set")
|
||||
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
|
||||
})
|
||||
t.Run("Get an existing composed key", func(t *testing.T) {
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
err = statestore.Set(&state.SetRequest{Key: "test-app||test-key", Value: []byte("test-value")})
|
||||
assert.Nil(t, err)
|
||||
getResponse, err := statestore.Get(&state.GetRequest{Key: "test-app||test-key"})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set")
|
||||
})
|
||||
t.Run("Get an unexpired state element with TTL set", func(t *testing.T) {
|
||||
testKey := "unexpired-ttl-test-key"
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
|
||||
"ttlInSeconds": "100",
|
||||
})})
|
||||
assert.Nil(t, err)
|
||||
getResponse, err := statestore.Get(&state.GetRequest{Key: testKey})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set despite TTL setting")
|
||||
})
|
||||
t.Run("Get a state element with TTL set to -1 (not expire)", func(t *testing.T) {
|
||||
testKey := "never-expiring-ttl-test-key"
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
|
||||
"ttlInSeconds": "-1",
|
||||
})})
|
||||
assert.Nil(t, err)
|
||||
getResponse, err := statestore.Get(&state.GetRequest{Key: testKey})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal (TTL setting of -1 means never expire)")
|
||||
})
|
||||
t.Run("Get an expired (TTL in the past) state element", func(t *testing.T) {
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
err = statestore.Set(&state.SetRequest{Key: "ttl-test-key", Value: []byte("test-value"), Metadata: (map[string]string{
|
||||
"ttlInSeconds": "1",
|
||||
})})
|
||||
assert.Nil(t, err)
|
||||
time.Sleep(time.Second * 2)
|
||||
getResponse, err := statestore.Get(&state.GetRequest{Key: "ttl-test-key"})
|
||||
assert.Equal(t, &state.GetResponse{}, getResponse, "Response must be empty")
|
||||
assert.NoError(t, err, "Expired element must not be treated as error")
|
||||
})
|
||||
}
|
||||
|
||||
func testSet(t *testing.T, ociProperties map[string]string) {
|
||||
meta := state.Metadata{}
|
||||
meta.Properties = ociProperties
|
||||
statestore := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
t.Run("Set without a key", func(t *testing.T) {
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
err = statestore.Set(&state.SetRequest{Value: []byte("test-value")})
|
||||
assert.Equal(t, err, fmt.Errorf("key for value to set was missing from request"), "Lacking Key results in error")
|
||||
})
|
||||
t.Run("Regular Set Operation", func(t *testing.T) {
|
||||
testKey := "local-test-key"
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
|
||||
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
|
||||
getResponse, err := statestore.Get(&state.GetRequest{Key: testKey})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set")
|
||||
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
|
||||
})
|
||||
t.Run("Regular Set Operation with composite key", func(t *testing.T) {
|
||||
testKey := "test-app||other-test-key"
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
|
||||
assert.Nil(t, err, "Setting a value with a proper composite key should be errorfree")
|
||||
getResponse, err := statestore.Get(&state.GetRequest{Key: testKey})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test-value", string(getResponse.Data), "Value retrieved should be equal to value set")
|
||||
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
|
||||
})
|
||||
t.Run("Regular Set Operation with TTL", func(t *testing.T) {
|
||||
testKey := "test-key-with-ttl"
|
||||
err := statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
|
||||
"ttlInSeconds": "500",
|
||||
})})
|
||||
assert.Nil(t, err, "Setting a value with a proper key and a correct TTL value should be errorfree")
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
|
||||
"ttlInSeconds": "XXX",
|
||||
})})
|
||||
assert.NotNil(t, err, "Setting a value with a proper key and a incorrect TTL value should be produce an error")
|
||||
})
|
||||
|
||||
t.Run("Testing Set & Concurrency (ETags)", func(t *testing.T) {
|
||||
testKey := "etag-test-key"
|
||||
err := statestore.Init(meta)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
|
||||
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
|
||||
getResponse, _ := statestore.Get(&state.GetRequest{Key: testKey})
|
||||
etag := getResponse.ETag
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("overwritten-value"), ETag: etag, Options: state.SetStateOption{
|
||||
Concurrency: state.FirstWrite,
|
||||
}})
|
||||
assert.Nil(t, err, "Updating value with proper etag should go fine")
|
||||
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("more-overwritten-value"), ETag: etag, Options: state.SetStateOption{
|
||||
Concurrency: state.FirstWrite,
|
||||
}})
|
||||
assert.NotNil(t, err, "Updating value with the old etag should be refused")
|
||||
|
||||
// retrieve the latest etag - assigned by the previous set operation.
|
||||
getResponse, _ = statestore.Get(&state.GetRequest{Key: testKey})
|
||||
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
|
||||
etag = getResponse.ETag
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("more-overwritten-value"), ETag: etag, Options: state.SetStateOption{
|
||||
Concurrency: state.FirstWrite,
|
||||
}})
|
||||
assert.Nil(t, err, "Updating value with the latest etag should be accepted")
|
||||
})
|
||||
}
|
||||
|
||||
func testDelete(t *testing.T, ociProperties map[string]string) {
|
||||
m := state.Metadata{}
|
||||
m.Properties = ociProperties
|
||||
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
t.Run("Delete without a key", func(t *testing.T) {
|
||||
err := s.Init(m)
|
||||
assert.Nil(t, err)
|
||||
err = s.Delete(&state.DeleteRequest{})
|
||||
assert.Equal(t, err, fmt.Errorf("key for value to delete was missing from request"), "Lacking Key results in error")
|
||||
})
|
||||
t.Run("Regular Delete Operation", func(t *testing.T) {
|
||||
testKey := "test-key"
|
||||
err := s.Init(m)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = s.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
|
||||
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
|
||||
err = s.Delete(&state.DeleteRequest{Key: testKey})
|
||||
assert.Nil(t, err, "Deleting an existing value with a proper key should be errorfree")
|
||||
})
|
||||
t.Run("Regular Delete Operation for composite key", func(t *testing.T) {
|
||||
testKey := "test-app||some-test-key"
|
||||
err := s.Init(m)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = s.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
|
||||
assert.Nil(t, err, "Setting a value with a proper composite key should be errorfree")
|
||||
err = s.Delete(&state.DeleteRequest{Key: testKey})
|
||||
assert.Nil(t, err, "Deleting an existing value with a proper composite key should be errorfree")
|
||||
})
|
||||
t.Run("Delete with an unknown key", func(t *testing.T) {
|
||||
err := s.Delete(&state.DeleteRequest{Key: "unknownKey"})
|
||||
assert.Contains(t, err.Error(), "404", "Unknown Key results in error: http status code 404, object not found")
|
||||
})
|
||||
|
||||
t.Run("Testing Delete & Concurrency (ETags)", func(t *testing.T) {
|
||||
testKey := "etag-test-delete-key"
|
||||
err := s.Init(m)
|
||||
assert.Nil(t, err)
|
||||
// create document.
|
||||
err = s.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
|
||||
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
|
||||
getResponse, _ := s.Get(&state.GetRequest{Key: testKey})
|
||||
etag := getResponse.ETag
|
||||
|
||||
incorrectETag := "someRandomETag"
|
||||
err = s.Delete(&state.DeleteRequest{Key: testKey, ETag: &incorrectETag, Options: state.DeleteStateOption{
|
||||
Concurrency: state.FirstWrite,
|
||||
}})
|
||||
assert.NotNil(t, err, "Deleting value with an incorrect etag should be prevented")
|
||||
|
||||
err = s.Delete(&state.DeleteRequest{Key: testKey, ETag: etag, Options: state.DeleteStateOption{
|
||||
Concurrency: state.FirstWrite,
|
||||
}})
|
||||
assert.Nil(t, err, "Deleting value with proper etag should go fine")
|
||||
})
|
||||
}
|
||||
|
||||
func testPing(t *testing.T, ociProperties map[string]string) {
|
||||
m := state.Metadata{}
|
||||
m.Properties = ociProperties
|
||||
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
t.Run("Ping", func(t *testing.T) {
|
||||
err := s.Init(m)
|
||||
assert.Nil(t, err)
|
||||
err = s.Ping()
|
||||
assert.Nil(t, err, "Ping should be successful")
|
||||
})
|
||||
}
|
||||
|
|
@ -1,14 +1,25 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
/*
|
||||
Copyright 2021 The Dapr Authors
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package objectstorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
|
|
@ -19,19 +30,20 @@ import (
|
|||
func getDummyOCIObjectStorageConfiguration() map[string]string {
|
||||
return map[string]string{
|
||||
"bucketName": "myBuck",
|
||||
"tenancyOCID": "ocid1.tenancy.oc1..aaaaaaaag7c7sq",
|
||||
"userOCID": "ocid1.user.oc1..aaaaaaaaby",
|
||||
"compartmentOCID": "ocid1.compartment.oc1..aaaaaaaq",
|
||||
"fingerPrint": "02:91:6c",
|
||||
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogI=\n-----END RSA PRIVATE KEY-----",
|
||||
"region": "us-ashburn-1",
|
||||
"tenancyOCID": "xxxocid1.tenancy.oc1..aaaaaaaag7c7sq",
|
||||
"userOCID": "xxxxocid1.user.oc1..aaaaaaaaby",
|
||||
"compartmentOCID": "xxxocid1.compartment.oc1..aaaaaaaq",
|
||||
"fingerPrint": "xxx02:91:6c",
|
||||
"privateKey": "xxx-----BEGIN RSA PRIVATE KEY-----\nMIIEogI=\n-----END RSA PRIVATE KEY-----",
|
||||
"region": "xxxus-ashburn-1",
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
meta := state.Metadata{}
|
||||
statestore := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
t.Run("Init with complete yet incorrect metadata", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Init with beautifully complete yet incorrect metadata", func(t *testing.T) {
|
||||
meta.Properties = getDummyOCIObjectStorageConfiguration()
|
||||
err := statestore.Init(meta)
|
||||
assert.NotNil(t, err)
|
||||
|
|
@ -80,9 +92,62 @@ func TestInit(t *testing.T) {
|
|||
assert.NotNil(t, err)
|
||||
assert.Equal(t, fmt.Errorf("missing or empty privateKey field from metadata"), err, "Lacking configuration property should be spotted")
|
||||
})
|
||||
t.Run("Init with incorrect value for instancePrincipalAuthentication", func(t *testing.T) {
|
||||
meta.Properties = getDummyOCIObjectStorageConfiguration()
|
||||
meta.Properties[instancePrincipalAuthenticationKey] = "ZQWE"
|
||||
err := statestore.Init(meta)
|
||||
assert.NotNil(t, err, "if instancePrincipalAuthentication is defined, it should be true or false; if not: error should be raised ")
|
||||
})
|
||||
t.Run("Init with missing fingerprint with instancePrincipalAuthentication", func(t *testing.T) {
|
||||
meta.Properties = getDummyOCIObjectStorageConfiguration()
|
||||
meta.Properties[fingerPrintKey] = ""
|
||||
meta.Properties[instancePrincipalAuthenticationKey] = "true"
|
||||
err := statestore.Init(meta)
|
||||
if err != nil {
|
||||
assert.Contains(t, err.Error(), "failed to initialize client", "unit tests not run on OCI will not be able to correctly create an OCI client based on instance principal authentication")
|
||||
}
|
||||
})
|
||||
t.Run("Init with configFileAuthentication and incorrect configFilePath", func(t *testing.T) {
|
||||
meta.Properties = getDummyOCIObjectStorageConfiguration()
|
||||
meta.Properties[configFileAuthenticationKey] = "true"
|
||||
meta.Properties[configFilePathKey] = "file_does_not_exist"
|
||||
err := statestore.Init(meta)
|
||||
assert.NotNil(t, err, "if configFileAuthentication is true and configFilePath does not indicate an existing file, then an error should be produced")
|
||||
if err != nil {
|
||||
assert.Contains(t, err.Error(), "does not exist", "if configFileAuthentication is true and configFilePath does not indicate an existing file, then an error should be produced that indicates this")
|
||||
}
|
||||
})
|
||||
t.Run("Init with configFileAuthentication and configFilePath starting with ~/", func(t *testing.T) {
|
||||
meta.Properties = getDummyOCIObjectStorageConfiguration()
|
||||
meta.Properties[configFileAuthenticationKey] = "true"
|
||||
meta.Properties[configFilePathKey] = "~/some-file"
|
||||
err := statestore.Init(meta)
|
||||
assert.NotNil(t, err, "if configFileAuthentication is true and configFilePath contains a value that starts with ~/ , then an error should be produced")
|
||||
if err != nil {
|
||||
assert.Contains(t, err.Error(), "~", "if configFileAuthentication is true and configFilePath starts with ~/, then an error should be produced that indicates this")
|
||||
}
|
||||
})
|
||||
t.Run("Init with missing fingerprint with false instancePrincipalAuthentication and false configFileAuthentication", func(t *testing.T) {
|
||||
meta.Properties = getDummyOCIObjectStorageConfiguration()
|
||||
meta.Properties[fingerPrintKey] = ""
|
||||
meta.Properties[instancePrincipalAuthenticationKey] = "false"
|
||||
err := statestore.Init(meta)
|
||||
assert.NotNil(t, err, "if instancePrincipalAuthentication and configFileAuthentication are both false, then fingerprint is required and an error should be raised when it is missing")
|
||||
})
|
||||
t.Run("Init with missing fingerprint with configFileAuthentication", func(t *testing.T) {
|
||||
meta.Properties = getDummyOCIObjectStorageConfiguration()
|
||||
meta.Properties[fingerPrintKey] = ""
|
||||
meta.Properties[instancePrincipalAuthenticationKey] = "false"
|
||||
meta.Properties[configFileAuthenticationKey] = "true"
|
||||
err := statestore.Init(meta)
|
||||
if err != nil {
|
||||
assert.Contains(t, err.Error(), "failed to initialize client", "if configFileAuthentication is true, then fingerprint is not required and error should be raised for failed to initialize client, not for missing fingerprint")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFeatures(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
t.Run("Test contents of Features", func(t *testing.T) {
|
||||
features := s.Features()
|
||||
|
|
@ -90,16 +155,52 @@ func TestFeatures(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
type mockedObjectStoreClient struct {
|
||||
objectStoreClient
|
||||
func TestGetObjectStorageMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Test getObjectStorageMetadata with full properties map", func(t *testing.T) {
|
||||
meta, err := getObjectStorageMetadata(getDummyOCIObjectStorageConfiguration())
|
||||
assert.Nil(t, err, "No error expected in clean property set")
|
||||
assert.Equal(t, getDummyOCIObjectStorageConfiguration()["region"], meta.region, "Region in object storage metadata should match region in properties")
|
||||
})
|
||||
t.Run("Test getObjectStorageMetadata with incomplete property set", func(t *testing.T) {
|
||||
properties := map[string]string{
|
||||
"region": "xxxus-ashburn-1",
|
||||
}
|
||||
_, err := getObjectStorageMetadata(properties)
|
||||
assert.NotNil(t, err, "Error expected with incomplete property set")
|
||||
})
|
||||
}
|
||||
|
||||
func (c *mockedObjectStoreClient) getObject(ctx context.Context, objectname string, logger logger.Logger) ([]byte, *string, error) {
|
||||
etag := "etag"
|
||||
return []byte("Hello World"), &etag, nil
|
||||
type mockedObjectStoreClient struct {
|
||||
ociObjectStorageClient
|
||||
getIsCalled bool
|
||||
putIsCalled bool
|
||||
deleteIsCalled bool
|
||||
pingBucketIsCalled bool
|
||||
}
|
||||
|
||||
func (c *mockedObjectStoreClient) getObject(ctx context.Context, objectname string, logger logger.Logger) (content []byte, etag *string, metadata map[string]string, err error) {
|
||||
c.getIsCalled = true
|
||||
etagString := "etag"
|
||||
contentString := "Hello World"
|
||||
metadata = map[string]string{}
|
||||
|
||||
if objectname == "unknownKey" {
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
if objectname == "test-expired-ttl-key" {
|
||||
metadata[expiryTimeMetaLabel] = time.Now().UTC().Add(time.Second * -10).Format(isoDateTimeFormat)
|
||||
}
|
||||
|
||||
if objectname == "test-app/test-key" {
|
||||
contentString = "Hello Continent"
|
||||
}
|
||||
|
||||
return []byte(contentString), &etagString, metadata, nil
|
||||
}
|
||||
|
||||
func (c *mockedObjectStoreClient) deleteObject(ctx context.Context, objectname string, etag *string) (err error) {
|
||||
c.deleteIsCalled = true
|
||||
if objectname == "unknownKey" {
|
||||
return fmt.Errorf("failed to delete object that does not exist - HTTP status code 404")
|
||||
}
|
||||
|
|
@ -110,6 +211,7 @@ func (c *mockedObjectStoreClient) deleteObject(ctx context.Context, objectname s
|
|||
}
|
||||
|
||||
func (c *mockedObjectStoreClient) putObject(ctx context.Context, objectname string, contentLen int64, content io.ReadCloser, metadata map[string]string, etag *string, logger logger.Logger) error {
|
||||
c.putIsCalled = true
|
||||
if etag != nil && *etag == "notTheCorrectETag" {
|
||||
return fmt.Errorf("failed to delete object because of incorrect etag-value ")
|
||||
}
|
||||
|
|
@ -124,22 +226,41 @@ func (c *mockedObjectStoreClient) initStorageBucket(logger logger.Logger) error
|
|||
}
|
||||
|
||||
func (c *mockedObjectStoreClient) pingBucket(logger logger.Logger) error {
|
||||
c.pingBucketIsCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestGetWithMockClient(t *testing.T) {
|
||||
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
s.client = &mockedObjectStoreClient{}
|
||||
|
||||
t.Run("Test contents of Features", func(t *testing.T) {
|
||||
mockClient := &mockedObjectStoreClient{}
|
||||
s.client = mockClient
|
||||
t.Parallel()
|
||||
t.Run("Test regular Get", func(t *testing.T) {
|
||||
getResponse, err := s.Get(&state.GetRequest{Key: "test-key"})
|
||||
assert.True(t, mockClient.getIsCalled, "function Get should be invoked on the mockClient")
|
||||
assert.Equal(t, "Hello World", string(getResponse.Data), "Value retrieved should be equal to value set")
|
||||
assert.NotNil(t, *getResponse.ETag, "ETag should be set")
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
t.Run("Test Get with composite key", func(t *testing.T) {
|
||||
getResponse, err := s.Get(&state.GetRequest{Key: "test-app||test-key"})
|
||||
assert.Equal(t, "Hello Continent", string(getResponse.Data), "Value retrieved should be equal to value set")
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
t.Run("Test Get with an unknown key", func(t *testing.T) {
|
||||
getResponse, err := s.Get(&state.GetRequest{Key: "unknownKey"})
|
||||
assert.Nil(t, getResponse.Data, "No value should be retrieved for an unknown key")
|
||||
assert.Nil(t, err, "404", "Not finding an object because of unknown key should not result in an error")
|
||||
})
|
||||
t.Run("Test expired element (because of TTL) ", func(t *testing.T) {
|
||||
getResponse, err := s.Get(&state.GetRequest{Key: "test-expired-ttl-key"})
|
||||
assert.Nil(t, getResponse.Data, "No value should be retrieved for an expired state element")
|
||||
assert.Nil(t, err, "Not returning an object because of expiration should not result in an error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitWithMockClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
s.client = &mockedObjectStoreClient{}
|
||||
meta := state.Metadata{}
|
||||
|
|
@ -150,18 +271,23 @@ func TestInitWithMockClient(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPingWithMockClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
s.client = &mockedObjectStoreClient{}
|
||||
mockClient := &mockedObjectStoreClient{}
|
||||
s.client = mockClient
|
||||
|
||||
t.Run("Test Ping", func(t *testing.T) {
|
||||
err := s.Ping()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, mockClient.pingBucketIsCalled, "function pingBucket should be invoked on the mockClient")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetWithMockClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
statestore := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
statestore.client = &mockedObjectStoreClient{}
|
||||
mockClient := &mockedObjectStoreClient{}
|
||||
statestore.client = mockClient
|
||||
t.Run("Set without a key", func(t *testing.T) {
|
||||
err := statestore.Set(&state.SetRequest{Value: []byte("test-value")})
|
||||
assert.Equal(t, err, fmt.Errorf("key for value to set was missing from request"), "Lacking Key results in error")
|
||||
|
|
@ -170,6 +296,24 @@ func TestSetWithMockClient(t *testing.T) {
|
|||
testKey := "test-key"
|
||||
err := statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value")})
|
||||
assert.Nil(t, err, "Setting a value with a proper key should be errorfree")
|
||||
assert.True(t, mockClient.putIsCalled, "function put should be invoked on the mockClient")
|
||||
})
|
||||
t.Run("Regular Set Operation with TTL", func(t *testing.T) {
|
||||
testKey := "test-key"
|
||||
err := statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
|
||||
"ttlInSeconds": "5",
|
||||
})})
|
||||
assert.Nil(t, err, "Setting a value with a proper key and a correct TTL value should be errorfree")
|
||||
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
|
||||
"ttlInSeconds": "XXX",
|
||||
})})
|
||||
assert.NotNil(t, err, "Setting a value with a proper key and a incorrect TTL value should be produce an error")
|
||||
|
||||
err = statestore.Set(&state.SetRequest{Key: testKey, Value: []byte("test-value"), Metadata: (map[string]string{
|
||||
"ttlInSeconds": "1",
|
||||
})})
|
||||
assert.Nil(t, err, "Setting a value with a proper key and a correct TTL value should be errorfree")
|
||||
})
|
||||
t.Run("Testing Set & Concurrency (ETags)", func(t *testing.T) {
|
||||
testKey := "etag-test-key"
|
||||
|
|
@ -199,8 +343,10 @@ func TestSetWithMockClient(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeleteWithMockClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := NewOCIObjectStorageStore(logger.NewLogger("logger"))
|
||||
s.client = &mockedObjectStoreClient{}
|
||||
mockClient := &mockedObjectStoreClient{}
|
||||
s.client = mockClient
|
||||
t.Run("Delete without a key", func(t *testing.T) {
|
||||
err := s.Delete(&state.DeleteRequest{})
|
||||
assert.Equal(t, err, fmt.Errorf("key for value to delete was missing from request"), "Lacking Key results in error")
|
||||
|
|
@ -213,6 +359,7 @@ func TestDeleteWithMockClient(t *testing.T) {
|
|||
testKey := "test-key"
|
||||
err := s.Delete(&state.DeleteRequest{Key: testKey})
|
||||
assert.Nil(t, err, "Deleting an existing value with a proper key should be errorfree")
|
||||
assert.True(t, mockClient.deleteIsCalled, "function delete should be invoked on the mockClient")
|
||||
})
|
||||
t.Run("Testing Delete & Concurrency (ETags)", func(t *testing.T) {
|
||||
testKey := "etag-test-delete-key"
|
||||
|
|
@ -234,3 +381,70 @@ func TestDeleteWithMockClient(t *testing.T) {
|
|||
assert.NotNil(t, err, "Asking for FirstWrite concurrency policy without ETag should fail")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetValue(t *testing.T) {
|
||||
meta := map[string]string{
|
||||
"testKey": "theValue",
|
||||
}
|
||||
t.Parallel()
|
||||
t.Run("Existing value", func(t *testing.T) {
|
||||
value, _ := getValue(meta, "testKey", true)
|
||||
assert.Equal(t, "theValue", value)
|
||||
})
|
||||
t.Run("Non-existing, required value", func(t *testing.T) {
|
||||
value, err := getValue(meta, "noKey", true)
|
||||
assert.NotNil(t, err, "Missing required value should result in error")
|
||||
assert.Equal(t, "", value, "Empty string should be returned for non-existing value")
|
||||
})
|
||||
t.Run("Non-existing, optional value", func(t *testing.T) {
|
||||
value, err := getValue(meta, "noKey", false)
|
||||
assert.Nil(t, err, "Missing optional value should not result in error")
|
||||
assert.Equal(t, "", value, "Empty string should be returned for non-existing value")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFilename(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Valid composite key", func(t *testing.T) {
|
||||
filename := getFileName("app-id||key")
|
||||
assert.Equal(t, "app-id/key", filename)
|
||||
})
|
||||
|
||||
t.Run("Normal (not-composite) key", func(t *testing.T) {
|
||||
filename := getFileName("app-id-key")
|
||||
assert.Equal(t, "app-id-key", filename)
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseTTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("TTL Not an integer", func(t *testing.T) {
|
||||
ttlInSeconds := "not an integer"
|
||||
ttl, err := parseTTL(map[string]string{
|
||||
"ttlInSeconds": ttlInSeconds,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, ttl)
|
||||
})
|
||||
t.Run("TTL specified with wrong key", func(t *testing.T) {
|
||||
ttlInSeconds := 12345
|
||||
ttl, err := parseTTL(map[string]string{
|
||||
"expirationTime": strconv.Itoa(ttlInSeconds),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, ttl)
|
||||
})
|
||||
t.Run("TTL is a number", func(t *testing.T) {
|
||||
ttlInSeconds := 12345
|
||||
ttl, err := parseTTL(map[string]string{
|
||||
"ttlInSeconds": strconv.Itoa(ttlInSeconds),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, *ttl, ttlInSeconds)
|
||||
})
|
||||
t.Run("TTL not set", func(t *testing.T) {
|
||||
ttl, err := parseTTL(map[string]string{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, ttl)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue