From 21e823b9553e11acb43517abaf8a36aa9612247b Mon Sep 17 00:00:00 2001 From: Atharva Zade Date: Tue, 13 May 2025 15:39:31 +0530 Subject: [PATCH] Initial Commit of Redis Plugin with Unit Test cases --- pkg/plugin/implementation/cache/cache.go | 16 +- pkg/plugin/implementation/cache/cache_test.go | 275 +++++++++--------- .../implementation/cache/cmd/plugin_test.go | 24 +- 3 files changed, 170 insertions(+), 145 deletions(-) diff --git a/pkg/plugin/implementation/cache/cache.go b/pkg/plugin/implementation/cache/cache.go index a368faf..4651302 100644 --- a/pkg/plugin/implementation/cache/cache.go +++ b/pkg/plugin/implementation/cache/cache.go @@ -10,12 +10,21 @@ import ( "github.com/redis/go-redis/v9" ) +// RedisClient is an interface for Redis operations that allows mocking +type RedisClient interface { + Get(ctx context.Context, key string) *redis.StringCmd + Set(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.StatusCmd + Del(ctx context.Context, keys ...string) *redis.IntCmd + FlushDB(ctx context.Context) *redis.StatusCmd + Ping(ctx context.Context) *redis.StatusCmd +} + type Config struct { Addr string } type Cache struct { - client *redis.Client + client RedisClient } var ( @@ -40,12 +49,7 @@ func New(ctx context.Context, cfg *Config) (*Cache, func() error, error) { return nil, nil, err } - // Get password from environment variable password := os.Getenv("REDIS_PASSWORD") - // Allow empty password for local testing - // if password == "" { - // return nil, nil, ErrCredentialMissing - // } client := redis.NewClient(&redis.Options{ Addr: cfg.Addr, diff --git a/pkg/plugin/implementation/cache/cache_test.go b/pkg/plugin/implementation/cache/cache_test.go index ddd7869..d90cc75 100644 --- a/pkg/plugin/implementation/cache/cache_test.go +++ b/pkg/plugin/implementation/cache/cache_test.go @@ -8,9 +8,93 @@ import ( "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) -// TestValidate tests the validation function for Cache configurations +// MockRedisClient is a mock implementation of the RedisClient interface +type MockRedisClient struct { + mock.Mock +} + +func (m *MockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd { + args := m.Called(ctx, key) + return redis.NewStringResult(args.String(0), args.Error(1)) +} + +func (m *MockRedisClient) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.StatusCmd { + args := m.Called(ctx, key, value, ttl) + return redis.NewStatusResult(args.String(0), args.Error(1)) +} + +func (m *MockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd { + args := m.Called(ctx, keys) + return redis.NewIntCmd(ctx, args.Int(0), args.Error(1)) +} + +func (m *MockRedisClient) FlushDB(ctx context.Context) *redis.StatusCmd { + args := m.Called(ctx) + return redis.NewStatusResult(args.String(0), args.Error(1)) +} + +func (m *MockRedisClient) Ping(ctx context.Context) *redis.StatusCmd { + args := m.Called(ctx) + return args.Get(0).(*redis.StatusCmd) +} + +// TestCache_Get tests the Get method of the Cache type +func TestCache_Get(t *testing.T) { + mockClient := new(MockRedisClient) + ctx := context.Background() + cache := &Cache{client: mockClient} + + mockClient.On("Get", ctx, "my-key").Return("my-value", nil) + + value, err := cache.Get(ctx, "my-key") + assert.NoError(t, err) + assert.Equal(t, "my-value", value) + mockClient.AssertExpectations(t) +} + +// TestCache_Set tests the Set method of the Cache type +func TestCache_Set(t *testing.T) { + mockClient := new(MockRedisClient) + ctx := context.Background() + cache := &Cache{client: mockClient} + + mockClient.On("Set", ctx, "my-key", "my-value", time.Minute).Return("OK", nil) + + err := cache.Set(ctx, "my-key", "my-value", time.Minute) + assert.NoError(t, err) + mockClient.AssertExpectations(t) +} + +// TestCache_Delete tests the Delete method of the Cache type +func TestCache_Delete(t *testing.T) { + mockClient := new(MockRedisClient) + ctx := context.Background() + cache := &Cache{client: mockClient} + + mockClient.On("Del", ctx, []string{"my-key"}).Return(1, nil) + + err := cache.Delete(ctx, "my-key") + assert.NoError(t, err) + mockClient.AssertExpectations(t) +} + +// TestCache_Clear tests the Clear method of the Cache type +func TestCache_Clear(t *testing.T) { + mockClient := new(MockRedisClient) + ctx := context.Background() + cache := &Cache{client: mockClient} + + mockClient.On("FlushDB", ctx).Return("OK", nil) + + err := cache.Clear(ctx) + assert.NoError(t, err) + mockClient.AssertExpectations(t) +} + +// TestValidate tests the validate function func TestValidate(t *testing.T) { tests := []struct { name string @@ -37,154 +121,75 @@ func TestValidate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validate(tt.cfg) - assert.Equal(t, tt.wantErr, err) - }) - } -} - -// TestNew tests the validation behavior of the constructor -func TestNew(t *testing.T) { - // Save original env and restore after test - origPassword := os.Getenv("REDIS_PASSWORD") - defer os.Setenv("REDIS_PASSWORD", origPassword) - - // Test validation errors directly - tests := []struct { - name string - cfg *Config - envPassword string - expectErr bool - errorContains string - }{ - { - name: "nil config", - cfg: nil, - envPassword: "password", - expectErr: true, - errorContains: "empty config", - }, - { - name: "empty address", - cfg: &Config{Addr: ""}, - envPassword: "password", - expectErr: true, - errorContains: "missing required field", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Set environment for this test - os.Setenv("REDIS_PASSWORD", tt.envPassword) - - ctx := context.Background() - cache, cleanup, err := New(ctx, tt.cfg) - - if tt.expectErr { - assert.Error(t, err) - assert.Nil(t, cache) - assert.Nil(t, cleanup) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } + if tt.wantErr != nil { + assert.Equal(t, tt.wantErr, err) } else { assert.NoError(t, err) - assert.NotNil(t, cache) - assert.NotNil(t, cleanup) } }) } } -// TestCache_Get tests the Get method of the Cache type -func TestCache_Get(t *testing.T) { - // Skip for now as we need to refactor to inject our mocks - t.Skip("Cache.Get test skipped - cannot inject mocks at this time") -} - -// TestCache_Set tests the Set method of the Cache type -func TestCache_Set(t *testing.T) { - // Skip for now as we need to refactor to inject our mocks - t.Skip("Cache.Set test skipped - cannot inject mocks at this time") -} - -// TestCache_Delete tests the Delete method of the Cache type -func TestCache_Delete(t *testing.T) { - // Skip for now as we need to refactor to inject our mocks - t.Skip("Cache.Delete test skipped - cannot inject mocks at this time") -} - -// TestCache_Clear tests the Clear method of the Cache type -func TestCache_Clear(t *testing.T) { - // Skip for now as we need to refactor to inject our mocks - t.Skip("Cache.Clear test skipped - cannot inject mocks at this time") -} - -// Integration test that tests all Redis operations with a real Redis server -func TestCacheIntegration(t *testing.T) { - // Run this test by default since we have a Redis server available - // To skip, set SKIP_REDIS_INTEGRATION_TEST=true - if os.Getenv("SKIP_REDIS_INTEGRATION_TEST") == "true" { - t.Skip("Integration test skipped - SKIP_REDIS_INTEGRATION_TEST=true") +// TestNew_Validation tests the validation parts of the New function +func TestNew_Validation(t *testing.T) { + testCases := []struct { + name string + cfg *Config + wantErr bool + errType error + }{ + { + name: "nil config", + cfg: nil, + wantErr: true, + errType: ErrEmptyConfig, + }, + { + name: "empty addr", + cfg: &Config{Addr: ""}, + wantErr: true, + errType: ErrAddrMissing, + }, } - // Set up test environment - ctx := context.Background() - cfg := &Config{ - Addr: "localhost:6379", - } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, _, err := New(context.Background(), tc.cfg) - // Set empty password for local testing - if err := os.Setenv("REDIS_PASSWORD", ""); err != nil { - t.Fatalf("Failed to set environment variable: %v", err) + if tc.wantErr { + assert.Error(t, err) + if tc.errType != nil { + assert.ErrorIs(t, err, tc.errType) + } + } else { + assert.NoError(t, err) + } + }) } - // Create a new cache - cache, cleanup, err := New(ctx, cfg) +} + +// TestNew_ConnectionFailure tests the connection failure in New function +func TestNew_ConnectionFailure(t *testing.T) { + // Set test env var + err := os.Setenv("REDIS_PASSWORD", "") if err != nil { - t.Fatalf("Failed to create cache: %v", err) + t.Fatalf("Failed to set REDIS_PASSWORD environment variable: %v", err) } - defer cleanup() + + defer func() { + err := os.Unsetenv("REDIS_PASSWORD") + if err != nil { + t.Fatalf("Failed to unset REDIS_PASSWORD environment variable: %v", err) + } + }() - // Test Set and Get - key := "test_key" - value := "test_value" - ttl := time.Minute + // Use an invalid connection address to force a connection failure + cfg := &Config{Addr: "invalid:1234"} - err = cache.Set(ctx, key, value, ttl) - assert.NoError(t, err, "Set should not return an error") - got, err := cache.Get(ctx, key) - assert.NoError(t, err, "Get should not return an error") - assert.Equal(t, value, got, "Get should return the set value") + // Call New which should fail with a connection error + _, _, err = New(context.Background(), cfg) - // Test Delete - err = cache.Delete(ctx, key) - assert.NoError(t, err, "Delete should not return an error") - - // Verify key is gone - _, err = cache.Get(ctx, key) - assert.Equal(t, redis.Nil, err, "Get should return redis.Nil after deletion") - - // Test Clear - // First set multiple keys - key1 := "test_key1" - value1 := "test_value1" - key2 := "test_key2" - value2 := "test_value2" - - err = cache.Set(ctx, key1, value1, ttl) - assert.NoError(t, err, "Set should not return an error") - - err = cache.Set(ctx, key2, value2, ttl) - assert.NoError(t, err, "Set should not return an error") - - // Clear all keys - err = cache.Clear(ctx) - assert.NoError(t, err, "Clear should not return an error") - - // Verify keys are gone - _, err = cache.Get(ctx, key1) - assert.Equal(t, redis.Nil, err, "Get should return redis.Nil after clear") - - _, err = cache.Get(ctx, key2) - assert.Equal(t, redis.Nil, err, "Get should return redis.Nil after clear") + // Verify error type is connection failure + assert.Error(t, err) + assert.ErrorIs(t, err, ErrConnectionFail) } diff --git a/pkg/plugin/implementation/cache/cmd/plugin_test.go b/pkg/plugin/implementation/cache/cmd/plugin_test.go index e16c27c..fdc83ff 100644 --- a/pkg/plugin/implementation/cache/cmd/plugin_test.go +++ b/pkg/plugin/implementation/cache/cmd/plugin_test.go @@ -87,10 +87,15 @@ func TestProviderNew(t *testing.T) { // Save original environment variable and restore it after test origPassword := os.Getenv("REDIS_PASSWORD") - defer os.Setenv("REDIS_PASSWORD", origPassword) - + defer func() { + if err := os.Setenv("REDIS_PASSWORD", origPassword); err != nil { + t.Fatalf("Failed to restore REDIS_PASSWORD: %v", err) + } + }() // Set an empty password for testing - os.Setenv("REDIS_PASSWORD", "") + if err := os.Setenv("REDIS_PASSWORD", ""); err != nil { + t.Fatalf("Failed to set REDIS_PASSWORD: %v", err) + } tests := []struct { name string @@ -140,6 +145,13 @@ func TestProviderIntegration(t *testing.T) { if err := os.Setenv("REDIS_PASSWORD", ""); err != nil { t.Fatalf("Failed to set REDIS_PASSWORD: %v", err) } + + // Ensure we clean up the environment variable at the end + defer func() { + if err := os.Unsetenv("REDIS_PASSWORD"); err != nil { + t.Fatalf("Failed to unset REDIS_PASSWORD: %v", err) + } + }() // Create provider and test with real Redis provider := cacheProvider{} @@ -153,7 +165,11 @@ func TestProviderIntegration(t *testing.T) { if err != nil { t.Fatalf("Failed to create cache: %v", err) } - defer cleanup() + defer func() { + if err := cleanup(); err != nil { + t.Fatalf("Failed to clean up Redis client: %v", err) + } + }() // Verify it works by setting and getting a value testKey := "provider_test_key"