diff --git a/.github/scripts/test-info.mjs b/.github/scripts/test-info.mjs index 1c4468af2..59e2a2729 100644 --- a/.github/scripts/test-info.mjs +++ b/.github/scripts/test-info.mjs @@ -266,6 +266,16 @@ const components = { 'crypto.jwks': { conformance: true, }, + 'lock.redis.v6': { + conformance: true, + conformanceSetup: 'docker-compose.sh redisjson redis', + sourcePkg: ['lock/redis', 'internal/component/redis'], + }, + 'lock.redis.v7': { + conformance: true, + conformanceSetup: 'docker-compose.sh redis7 redis', + sourcePkg: ['lock/redis', 'internal/component/redis'], + }, 'middleware.http.bearer': { certification: true, }, diff --git a/tests/config/lock/redis/v6/statestore.yaml b/tests/config/lock/redis/v6/statestore.yaml new file mode 100644 index 000000000..7684373e7 --- /dev/null +++ b/tests/config/lock/redis/v6/statestore.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: lockstore +spec: + type: lock.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" diff --git a/tests/config/lock/redis/v7/statestore.yaml b/tests/config/lock/redis/v7/statestore.yaml new file mode 100644 index 000000000..cae0f4ea3 --- /dev/null +++ b/tests/config/lock/redis/v7/statestore.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: lockstore +spec: + type: lock.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6380 + - name: redisPassword + value: "" diff --git a/tests/config/lock/tests.yml b/tests/config/lock/tests.yml new file mode 100644 index 000000000..1eafeb33f --- /dev/null +++ b/tests/config/lock/tests.yml @@ -0,0 +1,7 @@ +# Supported additional operations: (none) +componentType: lock +components: + - component: redis.v6 + operations: [] + - component: redis.v7 + operations: [] diff --git a/tests/conformance/bindings_test.go b/tests/conformance/bindings_test.go index 061804f74..25348742d 100644 --- a/tests/conformance/bindings_test.go +++ b/tests/conformance/bindings_test.go @@ -19,12 +19,12 @@ package conformance import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBindingsConformance(t *testing.T) { tc, err := NewTestConfiguration("../config/bindings/tests.yml") - assert.NoError(t, err) - assert.NotNil(t, tc) + require.NoError(t, err) + require.NotNil(t, tc) tc.Run(t) } diff --git a/tests/conformance/common.go b/tests/conformance/common.go index bfd485099..f8e5f93dc 100644 --- a/tests/conformance/common.go +++ b/tests/conformance/common.go @@ -36,6 +36,7 @@ import ( "github.com/dapr/components-contrib/bindings" "github.com/dapr/components-contrib/configuration" contribCrypto "github.com/dapr/components-contrib/crypto" + "github.com/dapr/components-contrib/lock" "github.com/dapr/components-contrib/pubsub" "github.com/dapr/components-contrib/secretstores" "github.com/dapr/components-contrib/state" @@ -63,6 +64,7 @@ import ( cr_azurekeyvault "github.com/dapr/components-contrib/crypto/azure/keyvault" cr_jwks "github.com/dapr/components-contrib/crypto/jwks" cr_localstorage "github.com/dapr/components-contrib/crypto/localstorage" + l_redis "github.com/dapr/components-contrib/lock/redis" p_snssqs "github.com/dapr/components-contrib/pubsub/aws/snssqs" p_eventhubs "github.com/dapr/components-contrib/pubsub/azure/eventhubs" p_servicebusqueues "github.com/dapr/components-contrib/pubsub/azure/servicebus/queues" @@ -105,6 +107,7 @@ import ( conf_bindings "github.com/dapr/components-contrib/tests/conformance/bindings" conf_configuration "github.com/dapr/components-contrib/tests/conformance/configuration" conf_crypto "github.com/dapr/components-contrib/tests/conformance/crypto" + conf_lock "github.com/dapr/components-contrib/tests/conformance/lock" conf_pubsub "github.com/dapr/components-contrib/tests/conformance/pubsub" conf_secret "github.com/dapr/components-contrib/tests/conformance/secretstores" conf_state "github.com/dapr/components-contrib/tests/conformance/state" @@ -402,6 +405,15 @@ func (tc *TestConfiguration) Run(t *testing.T) { wf := loadWorkflow(comp) wfConfig := conf_workflows.NewTestConfig(comp.Component, comp.Operations, comp.Config) conf_workflows.ConformanceTests(t, props, wf, wfConfig) + case "lock": + filepath := fmt.Sprintf("../config/lock/%s", componentConfigPath) + props, err := tc.loadComponentsAndProperties(t, filepath) + require.NoErrorf(t, err, "error running conformance test for component %s", comp.Component) + component := loadLockStore(comp) + require.NotNil(t, component, "error running conformance test for component %s", comp.Component) + lockConfig, err := conf_lock.NewTestConfig(comp.Component, comp.Operations, comp.Config) + require.NoErrorf(t, err, "error running conformance test for component %s", comp.Component) + conf_lock.ConformanceTests(t, props, component, lockConfig) case "crypto": filepath := fmt.Sprintf("../config/crypto/%s", componentConfigPath) props, err := tc.loadComponentsAndProperties(t, filepath) @@ -525,6 +537,18 @@ func loadCryptoProvider(tc TestComponent) contribCrypto.SubtleCrypto { return component } +func loadLockStore(tc TestComponent) lock.Store { + var component lock.Store + switch tc.Component { + case redisv6: + component = l_redis.NewStandaloneRedisLock(testLogger) + case redisv7: + component = l_redis.NewStandaloneRedisLock(testLogger) + } + + return component +} + func loadStateStore(tc TestComponent) state.Store { var store state.Store switch tc.Component { diff --git a/tests/conformance/configuration_test.go b/tests/conformance/configuration_test.go index 202ac17f3..072a48d7a 100644 --- a/tests/conformance/configuration_test.go +++ b/tests/conformance/configuration_test.go @@ -19,12 +19,12 @@ package conformance import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConfigurationConformance(t *testing.T) { tc, err := NewTestConfiguration("../config/configuration/tests.yml") - assert.NoError(t, err) - assert.NotNil(t, tc) + require.NoError(t, err) + require.NotNil(t, tc) tc.Run(t) } diff --git a/tests/conformance/crypto_test.go b/tests/conformance/crypto_test.go index 8de8829da..bc6b9ce91 100644 --- a/tests/conformance/crypto_test.go +++ b/tests/conformance/crypto_test.go @@ -19,12 +19,12 @@ package conformance import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCryptoConformance(t *testing.T) { tc, err := NewTestConfiguration("../config/crypto/tests.yml") - assert.NoError(t, err) - assert.NotNil(t, tc) + require.NoError(t, err) + require.NotNil(t, tc) tc.Run(t) } diff --git a/tests/conformance/lock/lock.go b/tests/conformance/lock/lock.go new file mode 100644 index 000000000..acb04e1a1 --- /dev/null +++ b/tests/conformance/lock/lock.go @@ -0,0 +1,180 @@ +/* +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 state + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/lock" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/tests/conformance/utils" + "github.com/dapr/kit/config" +) + +type TestConfig struct { + utils.CommonConfig +} + +func NewTestConfig(component string, operations []string, configMap map[string]interface{}) (TestConfig, error) { + testConfig := TestConfig{ + CommonConfig: utils.CommonConfig{ + ComponentType: "lock", + ComponentName: component, + Operations: utils.NewStringSet(operations...), + }, + } + + err := config.Decode(configMap, &testConfig) + if err != nil { + return testConfig, err + } + + return testConfig, nil +} + +// ConformanceTests runs conf tests for lock stores. +func ConformanceTests(t *testing.T, props map[string]string, lockstore lock.Store, config TestConfig) { + // Test vars + key := strings.ReplaceAll(uuid.New().String(), "-", "") + t.Logf("Base key for test: %s", key) + + t.Run("init", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + err := lockstore.InitLockStore(ctx, lock.Metadata{Base: metadata.Base{ + Properties: props, + }}) + require.NoError(t, err) + }) + + // Don't run more tests if init failed + if t.Failed() { + t.Fatal("Init failed, stopping further tests") + } + + const lockOwner = "conftest" + lockKey1 := key + "-1" + lockKey2 := key + "-2" + + var expirationCh *time.Timer + + t.Run("TryLock", func(t *testing.T) { + // Acquire a lock + t.Run("acquire lock1", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := lockstore.TryLock(ctx, &lock.TryLockRequest{ + ResourceID: lockKey1, + LockOwner: lockOwner, + ExpiryInSeconds: 15, + }) + require.NoError(t, err) + require.NotNil(t, res) + assert.True(t, res.Success) + }) + + // Acquire a second lock (with a shorter expiration) + t.Run("acquire lock2", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := lockstore.TryLock(ctx, &lock.TryLockRequest{ + ResourceID: lockKey2, + LockOwner: lockOwner, + ExpiryInSeconds: 3, + }) + require.NoError(t, err) + require.NotNil(t, res) + assert.True(t, res.Success) + + // Set expirationCh to when lock2 expires + expirationCh = time.NewTimer(3 * time.Second) + }) + + // Acquiring the same lock again should fail + t.Run("fails to acquire existing lock", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := lockstore.TryLock(ctx, &lock.TryLockRequest{ + ResourceID: lockKey1, + LockOwner: lockOwner, + ExpiryInSeconds: 15, + }) + require.NoError(t, err) + require.NotNil(t, res) + assert.False(t, res.Success) + }) + }) + + t.Run("Unlock", func(t *testing.T) { + t.Run("fails to unlock with nonexistent resource ID", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := lockstore.Unlock(ctx, &lock.UnlockRequest{ + ResourceID: "nonexistent", + LockOwner: lockOwner, + }) + require.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, lock.LockDoesNotExist, res.Status) + }) + + t.Run("fails to unlock with wrong owner", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := lockstore.Unlock(ctx, &lock.UnlockRequest{ + ResourceID: lockKey1, + LockOwner: "nonowner", + }) + require.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, lock.LockBelongsToOthers, res.Status) + }) + + t.Run("unlocks successfully", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := lockstore.Unlock(ctx, &lock.UnlockRequest{ + ResourceID: lockKey1, + LockOwner: lockOwner, + }) + require.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, lock.Success, res.Status) + }) + }) + + t.Run("lock expires", func(t *testing.T) { + // Wait until the lock is supposed to expire + <-expirationCh.C + + // Assert that the lock doesn't exist anymore - we should be able to re-acquire it + assert.Eventually(t, func() bool { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := lockstore.TryLock(ctx, &lock.TryLockRequest{ + ResourceID: lockKey2, + LockOwner: lockOwner, + ExpiryInSeconds: 3, + }) + return err == nil && res != nil && res.Success + }, 5*time.Second, 100*time.Millisecond, "Lock 2 was not released in time after its scheduled expiration") + }) +} diff --git a/tests/conformance/lock_test.go b/tests/conformance/lock_test.go new file mode 100644 index 000000000..ce6bb3e93 --- /dev/null +++ b/tests/conformance/lock_test.go @@ -0,0 +1,30 @@ +//go:build conftests +// +build conftests + +/* +Copyright 2023 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 conformance + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLockConformance(t *testing.T) { + tc, err := NewTestConfiguration("../config/lock/tests.yml") + require.NoError(t, err) + require.NotNil(t, tc) + tc.Run(t) +} diff --git a/tests/conformance/pubsub_test.go b/tests/conformance/pubsub_test.go index 2e0e8453e..ed026d1bc 100644 --- a/tests/conformance/pubsub_test.go +++ b/tests/conformance/pubsub_test.go @@ -19,12 +19,12 @@ package conformance import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPubsubConformance(t *testing.T) { tc, err := NewTestConfiguration("../config/pubsub/tests.yml") - assert.NoError(t, err) - assert.NotNil(t, tc) + require.NoError(t, err) + require.NotNil(t, tc) tc.Run(t) } diff --git a/tests/conformance/secretstores_test.go b/tests/conformance/secretstores_test.go index 5d4c494c7..51b221b52 100644 --- a/tests/conformance/secretstores_test.go +++ b/tests/conformance/secretstores_test.go @@ -19,12 +19,12 @@ package conformance import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSecretStoreConformance(t *testing.T) { tc, err := NewTestConfiguration("../config/secretstores/tests.yml") - assert.NoError(t, err) - assert.NotNil(t, tc) + require.NoError(t, err) + require.NotNil(t, tc) tc.Run(t) } diff --git a/tests/conformance/state_test.go b/tests/conformance/state_test.go index 2886d6baf..a53e186e8 100644 --- a/tests/conformance/state_test.go +++ b/tests/conformance/state_test.go @@ -19,12 +19,12 @@ package conformance import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestStateConformance(t *testing.T) { tc, err := NewTestConfiguration("../config/state/tests.yml") - assert.NoError(t, err) - assert.NotNil(t, tc) + require.NoError(t, err) + require.NotNil(t, tc) tc.Run(t) } diff --git a/tests/conformance/workflow_test.go b/tests/conformance/workflow_test.go index 35aeb6d4e..e2fd49871 100644 --- a/tests/conformance/workflow_test.go +++ b/tests/conformance/workflow_test.go @@ -19,12 +19,12 @@ package conformance import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWorkflowsConformance(t *testing.T) { tc, err := NewTestConfiguration("../config/workflows/tests.yml") - assert.NoError(t, err) - assert.NotNil(t, tc) + require.NoError(t, err) + require.NotNil(t, tc) tc.Run(t) }