From 7d613ed495425306fa32a6e6751a30418a825525 Mon Sep 17 00:00:00 2001 From: Atharva Zade Date: Tue, 13 May 2025 13:01:09 +0530 Subject: [PATCH 1/7] Initial commit for Cache Plugin --- go.mod | 7 +- go.sum | 10 +++ pkg/plugin/implementation/cache/cache.go | 77 +++++++++++++++++++ pkg/plugin/implementation/cache/cmd/plugin.go | 77 +++++++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 pkg/plugin/implementation/cache/cache.go create mode 100644 pkg/plugin/implementation/cache/cmd/plugin.go diff --git a/go.mod b/go.mod index 12fad60..04f1e61 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/beckn/beckn-onix -go 1.24 +go 1.23.0 + +toolchain go1.23.4 require ( github.com/kr/pretty v0.3.1 // indirect @@ -25,6 +27,8 @@ require github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 require golang.org/x/text v0.23.0 // indirect require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/sys v0.31.0 // indirect @@ -32,6 +36,7 @@ require ( require ( github.com/hashicorp/go-retryablehttp v0.7.7 + github.com/redis/go-redis/v9 v9.8.0 github.com/rs/zerolog v1.34.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index 821e117..ccaf9d5 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,15 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -30,6 +38,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK 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/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= diff --git a/pkg/plugin/implementation/cache/cache.go b/pkg/plugin/implementation/cache/cache.go new file mode 100644 index 0000000..a368faf --- /dev/null +++ b/pkg/plugin/implementation/cache/cache.go @@ -0,0 +1,77 @@ +package cache + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + "github.com/redis/go-redis/v9" +) + +type Config struct { + Addr string +} + +type Cache struct { + client *redis.Client +} + +var ( + ErrEmptyConfig = errors.New("empty config") + ErrAddrMissing = errors.New("missing required field 'Addr'") + ErrCredentialMissing = errors.New("missing Redis credentials in environment") + ErrConnectionFail = errors.New("failed to connect to Redis") +) + +func validate(cfg *Config) error { + if cfg == nil { + return ErrEmptyConfig + } + if cfg.Addr == "" { + return ErrAddrMissing + } + return nil +} + +func New(ctx context.Context, cfg *Config) (*Cache, func() error, error) { + if err := validate(cfg); err != nil { + 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, + Password: password, + DB: 0, // Always use default DB 0 + }) + + if _, err := client.Ping(ctx).Result(); err != nil { + return nil, nil, fmt.Errorf("%w: %v", ErrConnectionFail, err) + } + + return &Cache{client: client}, client.Close, nil +} + +func (c *Cache) Get(ctx context.Context, key string) (string, error) { + return c.client.Get(ctx, key).Result() +} + +func (c *Cache) Set(ctx context.Context, key, value string, ttl time.Duration) error { + return c.client.Set(ctx, key, value, ttl).Err() +} + +func (c *Cache) Delete(ctx context.Context, key string) error { + return c.client.Del(ctx, key).Err() +} + +func (c *Cache) Clear(ctx context.Context) error { + return c.client.FlushDB(ctx).Err() +} diff --git a/pkg/plugin/implementation/cache/cmd/plugin.go b/pkg/plugin/implementation/cache/cmd/plugin.go new file mode 100644 index 0000000..07082ab --- /dev/null +++ b/pkg/plugin/implementation/cache/cmd/plugin.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "errors" + "strconv" + + "github.com/beckn/beckn-onix/pkg/plugin/definition" + "github.com/beckn/beckn-onix/pkg/plugin/implementation/cache" +) + +// cacheProvider implements the definition.CacheProvider interface. +type cacheProvider struct{} + +// Config holds the configuration settings for the Redis cache plugin. +type Config struct { + Addr string // Redis address (host:port) + DB int // Redis database number (optional, defaults to 0) + Password string // Redis password (optional, can be empty or from env) +} + +// parseConfig converts the string map configuration to a Config struct. +func parseConfig(config map[string]string) (*Config, error) { + addr, ok := config["addr"] + if !ok || addr == "" { + return nil, errors.New("config must contain 'addr'") + } + + // Default values + db := 0 + password := "" + + // Parse DB if provided + if val, ok := config["db"]; ok && val != "" { + if parsedVal, err := strconv.Atoi(val); err == nil { + db = parsedVal + } + } + + // Get password if provided + if val, ok := config["password"]; ok { + password = val + } + + return &Config{ + Addr: addr, + DB: db, + Password: password, + }, nil +} + +// convertToRedisConfig converts the plugin Config to redis.Config. +func convertToRedisConfig(cfg *Config) *cache.Config { + return &cache.Config{ + Addr: cfg.Addr, + } +} + +// New initializes a new Redis cache with the given configuration. +func (p cacheProvider) New(ctx context.Context, config map[string]string) (definition.Cache, func() error, error) { + if ctx == nil { + return nil, nil, errors.New("context cannot be nil") + } + + cfg, err := parseConfig(config) + if err != nil { + return nil, nil, err + } + + // Convert to redis.Config + redisConfig := convertToRedisConfig(cfg) + + return cache.New(ctx, redisConfig) +} + +// Provider is the exported symbol that the plugin manager will look for. +var Provider = cacheProvider{} From ec69ecd50db5586a2a538df517c2466b7ae1170f Mon Sep 17 00:00:00 2001 From: Atharva Zade Date: Tue, 13 May 2025 14:47:01 +0530 Subject: [PATCH 2/7] Initial Commit of Redis Plugin with Unit Test cases --- pkg/plugin/implementation/cache/cache_test.go | 264 ++++++++++++++++++ .../implementation/cache/cmd/plugin_test.go | 177 ++++++++++++ 2 files changed, 441 insertions(+) create mode 100644 pkg/plugin/implementation/cache/cache_test.go create mode 100644 pkg/plugin/implementation/cache/cmd/plugin_test.go diff --git a/pkg/plugin/implementation/cache/cache_test.go b/pkg/plugin/implementation/cache/cache_test.go new file mode 100644 index 0000000..931cb96 --- /dev/null +++ b/pkg/plugin/implementation/cache/cache_test.go @@ -0,0 +1,264 @@ +package cache + +import ( + "context" + "os" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// mockRedisClient is a mock implementation of the Redis client for testing +type mockRedisClient struct { + mock.Mock +} + +// Create a mock of all Redis client methods we use +func (m *mockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd { + args := m.Called(ctx, key) + return args.Get(0).(*redis.StringCmd) +} + +func (m *mockRedisClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + args := m.Called(ctx, key, value, expiration) + return args.Get(0).(*redis.StatusCmd) +} + +func (m *mockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd { + args := m.Called(ctx, keys) + return args.Get(0).(*redis.IntCmd) +} + +func (m *mockRedisClient) FlushDB(ctx context.Context) *redis.StatusCmd { + args := m.Called(ctx) + return args.Get(0).(*redis.StatusCmd) +} + +func (m *mockRedisClient) Ping(ctx context.Context) *redis.StatusCmd { + args := m.Called(ctx) + return args.Get(0).(*redis.StatusCmd) +} + +func (m *mockRedisClient) Close() error { + args := m.Called() + return args.Error(0) +} + +// Test helpers for creating Redis command responses +func stringCmdWithValue(val string) *redis.StringCmd { + cmd := redis.NewStringCmd(context.Background()) + cmd.SetVal(val) + return cmd +} + +func stringCmdWithError(err error) *redis.StringCmd { + cmd := redis.NewStringCmd(context.Background()) + cmd.SetErr(err) + return cmd +} + +func statusCmdSuccess() *redis.StatusCmd { + cmd := redis.NewStatusCmd(context.Background(), "OK") + return cmd +} + +func statusCmdWithError(err error) *redis.StatusCmd { + cmd := redis.NewStatusCmd(context.Background()) + cmd.SetErr(err) + return cmd +} + +func intCmdWithValue(val int64) *redis.IntCmd { + cmd := redis.NewIntCmd(context.Background()) + cmd.SetVal(val) + return cmd +} + +func intCmdWithError(err error) *redis.IntCmd { + cmd := redis.NewIntCmd(context.Background()) + cmd.SetErr(err) + return cmd +} + + +// TestValidate tests the validation function for Cache configurations +func TestValidate(t *testing.T) { + tests := []struct { + name string + cfg *Config + wantErr error + }{ + { + name: "nil config", + cfg: nil, + wantErr: ErrEmptyConfig, + }, + { + name: "empty addr", + cfg: &Config{Addr: ""}, + wantErr: ErrAddrMissing, + }, + { + name: "valid config", + cfg: &Config{Addr: "localhost:6379"}, + wantErr: nil, + }, + } + + 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) + } + } 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") + } + + // Set up test environment + ctx := context.Background() + cfg := &Config{ + Addr: "localhost:6379", + } + + // Set empty password for local testing + os.Setenv("REDIS_PASSWORD", "") + + // Create a new cache + cache, cleanup, err := New(ctx, cfg) + if err != nil { + t.Fatalf("Failed to create cache: %v", err) + } + defer cleanup() + + // Test Set and Get + key := "test_key" + value := "test_value" + ttl := time.Minute + + 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") + + // 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") +} diff --git a/pkg/plugin/implementation/cache/cmd/plugin_test.go b/pkg/plugin/implementation/cache/cmd/plugin_test.go new file mode 100644 index 0000000..322cb02 --- /dev/null +++ b/pkg/plugin/implementation/cache/cmd/plugin_test.go @@ -0,0 +1,177 @@ +package main + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestParseConfig tests the configuration parsing logic of the plugin +func TestParseConfig(t *testing.T) { + tests := []struct { + name string + config map[string]string + want *Config + wantErr bool + }{ + { + name: "missing addr", + config: map[string]string{}, + want: nil, + wantErr: true, + }, + { + name: "empty addr", + config: map[string]string{"addr": ""}, + want: nil, + wantErr: true, + }, + { + name: "basic config", + config: map[string]string{"addr": "localhost:6379"}, + want: &Config{Addr: "localhost:6379", DB: 0, Password: ""}, + wantErr: false, + }, + { + name: "with db", + config: map[string]string{"addr": "localhost:6379", "db": "1"}, + want: &Config{Addr: "localhost:6379", DB: 1, Password: ""}, + wantErr: false, + }, + { + name: "with password", + config: map[string]string{"addr": "localhost:6379", "password": "secret"}, + want: &Config{Addr: "localhost:6379", DB: 0, Password: "secret"}, + wantErr: false, + }, + { + name: "invalid db", + config: map[string]string{"addr": "localhost:6379", "db": "invalid"}, + want: &Config{Addr: "localhost:6379", DB: 0, Password: ""}, + wantErr: false, // Not an error, just defaults to 0 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseConfig(tt.config) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +// TestConvertToRedisConfig tests the configuration conversion logic +func TestConvertToRedisConfig(t *testing.T) { + cfg := &Config{ + Addr: "localhost:6379", + DB: 1, + Password: "secret", + } + + redisConfig := convertToRedisConfig(cfg) + + assert.NotNil(t, redisConfig) + assert.Equal(t, cfg.Addr, redisConfig.Addr) +} + +// TestProviderNew tests the New method of the cacheProvider +func TestProviderNew(t *testing.T) { + provider := cacheProvider{} + + // Save original environment variable and restore it after test + origPassword := os.Getenv("REDIS_PASSWORD") + defer os.Setenv("REDIS_PASSWORD", origPassword) + + // Set an empty password for testing + os.Setenv("REDIS_PASSWORD", "") + + tests := []struct { + name string + ctx context.Context + config map[string]string + expectErr bool + }{ + { + name: "nil context", + ctx: nil, + config: map[string]string{"addr": "localhost:6379"}, + expectErr: true, + }, + { + name: "invalid config", + ctx: context.Background(), + config: map[string]string{}, // Missing addr + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cache, cleanup, err := provider.New(tt.ctx, tt.config) + + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, cache) + assert.Nil(t, cleanup) + } else { + assert.NoError(t, err) + assert.NotNil(t, cache) + assert.NotNil(t, cleanup) + } + }) + } +} + +// TestProviderIntegration tests the provider with a real Redis server +func TestProviderIntegration(t *testing.T) { + // Skip this test if requested + if os.Getenv("SKIP_REDIS_INTEGRATION_TEST") == "true" { + t.Skip("Integration test skipped - SKIP_REDIS_INTEGRATION_TEST=true") + } + + // Set an empty password for testing + os.Setenv("REDIS_PASSWORD", "") + + // Create provider and test with real Redis + provider := cacheProvider{} + ctx := context.Background() + config := map[string]string{ + "addr": "localhost:6379", + "db": "0", + } + + cache, cleanup, err := provider.New(ctx, config) + if err != nil { + t.Fatalf("Failed to create cache: %v", err) + } + defer cleanup() + + // Verify it works by setting and getting a value + testKey := "provider_test_key" + testValue := "provider_test_value" + + // Set a value + err = cache.Set(ctx, testKey, testValue, 0) + assert.NoError(t, err, "Set operation should not fail") + + // Get the value + got, err := cache.Get(ctx, testKey) + assert.NoError(t, err, "Get operation should not fail") + assert.Equal(t, testValue, got, "Should get the value that was set") + + // Clean up + err = cache.Delete(ctx, testKey) + assert.NoError(t, err, "Delete operation should not fail") +} + +// TestProviderVariable tests that the Provider variable is correctly initialized +func TestProviderVariable(t *testing.T) { + assert.NotNil(t, Provider, "Provider should not be nil") +} From afd157d123a3cdd2205a71f79c8478bfea6786fe Mon Sep 17 00:00:00 2001 From: Atharva Zade Date: Tue, 13 May 2025 14:59:19 +0530 Subject: [PATCH 3/7] Initial Commit of Redis Plugin with Unit Test cases --- pkg/plugin/implementation/cache/cache_test.go | 112 +++--------------- .../implementation/cache/cmd/plugin_test.go | 10 +- 2 files changed, 25 insertions(+), 97 deletions(-) diff --git a/pkg/plugin/implementation/cache/cache_test.go b/pkg/plugin/implementation/cache/cache_test.go index 931cb96..ddd7869 100644 --- a/pkg/plugin/implementation/cache/cache_test.go +++ b/pkg/plugin/implementation/cache/cache_test.go @@ -8,82 +8,8 @@ import ( "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) -// mockRedisClient is a mock implementation of the Redis client for testing -type mockRedisClient struct { - mock.Mock -} - -// Create a mock of all Redis client methods we use -func (m *mockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd { - args := m.Called(ctx, key) - return args.Get(0).(*redis.StringCmd) -} - -func (m *mockRedisClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { - args := m.Called(ctx, key, value, expiration) - return args.Get(0).(*redis.StatusCmd) -} - -func (m *mockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd { - args := m.Called(ctx, keys) - return args.Get(0).(*redis.IntCmd) -} - -func (m *mockRedisClient) FlushDB(ctx context.Context) *redis.StatusCmd { - args := m.Called(ctx) - return args.Get(0).(*redis.StatusCmd) -} - -func (m *mockRedisClient) Ping(ctx context.Context) *redis.StatusCmd { - args := m.Called(ctx) - return args.Get(0).(*redis.StatusCmd) -} - -func (m *mockRedisClient) Close() error { - args := m.Called() - return args.Error(0) -} - -// Test helpers for creating Redis command responses -func stringCmdWithValue(val string) *redis.StringCmd { - cmd := redis.NewStringCmd(context.Background()) - cmd.SetVal(val) - return cmd -} - -func stringCmdWithError(err error) *redis.StringCmd { - cmd := redis.NewStringCmd(context.Background()) - cmd.SetErr(err) - return cmd -} - -func statusCmdSuccess() *redis.StatusCmd { - cmd := redis.NewStatusCmd(context.Background(), "OK") - return cmd -} - -func statusCmdWithError(err error) *redis.StatusCmd { - cmd := redis.NewStatusCmd(context.Background()) - cmd.SetErr(err) - return cmd -} - -func intCmdWithValue(val int64) *redis.IntCmd { - cmd := redis.NewIntCmd(context.Background()) - cmd.SetVal(val) - return cmd -} - -func intCmdWithError(err error) *redis.IntCmd { - cmd := redis.NewIntCmd(context.Background()) - cmd.SetErr(err) - return cmd -} - - // TestValidate tests the validation function for Cache configurations func TestValidate(t *testing.T) { tests := []struct { @@ -121,7 +47,7 @@ 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 @@ -145,15 +71,15 @@ func TestNew(t *testing.T) { 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) @@ -201,64 +127,64 @@ func TestCacheIntegration(t *testing.T) { if os.Getenv("SKIP_REDIS_INTEGRATION_TEST") == "true" { t.Skip("Integration test skipped - SKIP_REDIS_INTEGRATION_TEST=true") } - + // Set up test environment ctx := context.Background() cfg := &Config{ Addr: "localhost:6379", } - + // Set empty password for local testing - os.Setenv("REDIS_PASSWORD", "") - + if err := os.Setenv("REDIS_PASSWORD", ""); err != nil { + t.Fatalf("Failed to set environment variable: %v", err) + } // Create a new cache cache, cleanup, err := New(ctx, cfg) if err != nil { t.Fatalf("Failed to create cache: %v", err) } defer cleanup() - + // Test Set and Get key := "test_key" value := "test_value" ttl := time.Minute - + 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") - + // 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") } diff --git a/pkg/plugin/implementation/cache/cmd/plugin_test.go b/pkg/plugin/implementation/cache/cmd/plugin_test.go index 322cb02..e16c27c 100644 --- a/pkg/plugin/implementation/cache/cmd/plugin_test.go +++ b/pkg/plugin/implementation/cache/cmd/plugin_test.go @@ -137,7 +137,9 @@ func TestProviderIntegration(t *testing.T) { } // 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) + } // Create provider and test with real Redis provider := cacheProvider{} @@ -156,16 +158,16 @@ func TestProviderIntegration(t *testing.T) { // Verify it works by setting and getting a value testKey := "provider_test_key" testValue := "provider_test_value" - + // Set a value err = cache.Set(ctx, testKey, testValue, 0) assert.NoError(t, err, "Set operation should not fail") - + // Get the value got, err := cache.Get(ctx, testKey) assert.NoError(t, err, "Get operation should not fail") assert.Equal(t, testValue, got, "Should get the value that was set") - + // Clean up err = cache.Delete(ctx, testKey) assert.NoError(t, err, "Delete operation should not fail") From 21e823b9553e11acb43517abaf8a36aa9612247b Mon Sep 17 00:00:00 2001 From: Atharva Zade Date: Tue, 13 May 2025 15:39:31 +0530 Subject: [PATCH 4/7] 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" From 1b791566932524880508c9c2ddd6498ec42ee134 Mon Sep 17 00:00:00 2001 From: Atharva Zade Date: Tue, 13 May 2025 16:47:34 +0530 Subject: [PATCH 5/7] Initial Commit of Redis Plugin with Unit Test cases --- pkg/plugin/implementation/cache/cache.go | 32 +-- pkg/plugin/implementation/cache/cache_test.go | 15 +- .../implementation/cache/cmd/plugin_test.go | 184 +++++++++++++----- 3 files changed, 167 insertions(+), 64 deletions(-) diff --git a/pkg/plugin/implementation/cache/cache.go b/pkg/plugin/implementation/cache/cache.go index 4651302..c8c374a 100644 --- a/pkg/plugin/implementation/cache/cache.go +++ b/pkg/plugin/implementation/cache/cache.go @@ -10,6 +10,9 @@ import ( "github.com/redis/go-redis/v9" ) +// Global variable for the Redis client, can be overridden in tests +var RedisCl *redis.Client + // RedisClient is an interface for Redis operations that allows mocking type RedisClient interface { Get(ctx context.Context, key string) *redis.StringCmd @@ -17,6 +20,7 @@ type RedisClient interface { Del(ctx context.Context, keys ...string) *redis.IntCmd FlushDB(ctx context.Context) *redis.StatusCmd Ping(ctx context.Context) *redis.StatusCmd + Close() error } type Config struct { @@ -24,7 +28,7 @@ type Config struct { } type Cache struct { - client RedisClient + Client RedisClient } var ( @@ -44,38 +48,40 @@ func validate(cfg *Config) error { return nil } +var RedisClientFunc = func(cfg *Config) RedisClient { + return redis.NewClient(&redis.Options{ + Addr: cfg.Addr, + Password: os.Getenv("REDIS_PASSWORD"), + DB: 0, + }) +} + func New(ctx context.Context, cfg *Config) (*Cache, func() error, error) { if err := validate(cfg); err != nil { return nil, nil, err } - password := os.Getenv("REDIS_PASSWORD") - - client := redis.NewClient(&redis.Options{ - Addr: cfg.Addr, - Password: password, - DB: 0, // Always use default DB 0 - }) + client := RedisClientFunc(cfg) if _, err := client.Ping(ctx).Result(); err != nil { return nil, nil, fmt.Errorf("%w: %v", ErrConnectionFail, err) } - return &Cache{client: client}, client.Close, nil + return &Cache{Client: client}, client.Close, nil } func (c *Cache) Get(ctx context.Context, key string) (string, error) { - return c.client.Get(ctx, key).Result() + return c.Client.Get(ctx, key).Result() } func (c *Cache) Set(ctx context.Context, key, value string, ttl time.Duration) error { - return c.client.Set(ctx, key, value, ttl).Err() + return c.Client.Set(ctx, key, value, ttl).Err() } func (c *Cache) Delete(ctx context.Context, key string) error { - return c.client.Del(ctx, key).Err() + return c.Client.Del(ctx, key).Err() } func (c *Cache) Clear(ctx context.Context) error { - return c.client.FlushDB(ctx).Err() + return c.Client.FlushDB(ctx).Err() } diff --git a/pkg/plugin/implementation/cache/cache_test.go b/pkg/plugin/implementation/cache/cache_test.go index d90cc75..05113fe 100644 --- a/pkg/plugin/implementation/cache/cache_test.go +++ b/pkg/plugin/implementation/cache/cache_test.go @@ -41,11 +41,16 @@ func (m *MockRedisClient) Ping(ctx context.Context) *redis.StatusCmd { return args.Get(0).(*redis.StatusCmd) } +func (m *MockRedisClient) Close() error { + args := m.Called() + return args.Error(0) +} + // 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} + cache := &Cache{Client: mockClient} mockClient.On("Get", ctx, "my-key").Return("my-value", nil) @@ -59,7 +64,7 @@ func TestCache_Get(t *testing.T) { func TestCache_Set(t *testing.T) { mockClient := new(MockRedisClient) ctx := context.Background() - cache := &Cache{client: mockClient} + cache := &Cache{Client: mockClient} mockClient.On("Set", ctx, "my-key", "my-value", time.Minute).Return("OK", nil) @@ -72,7 +77,7 @@ func TestCache_Set(t *testing.T) { func TestCache_Delete(t *testing.T) { mockClient := new(MockRedisClient) ctx := context.Background() - cache := &Cache{client: mockClient} + cache := &Cache{Client: mockClient} mockClient.On("Del", ctx, []string{"my-key"}).Return(1, nil) @@ -85,7 +90,7 @@ func TestCache_Delete(t *testing.T) { func TestCache_Clear(t *testing.T) { mockClient := new(MockRedisClient) ctx := context.Background() - cache := &Cache{client: mockClient} + cache := &Cache{Client: mockClient} mockClient.On("FlushDB", ctx).Return("OK", nil) @@ -175,7 +180,7 @@ func TestNew_ConnectionFailure(t *testing.T) { if err != nil { t.Fatalf("Failed to set REDIS_PASSWORD environment variable: %v", err) } - + defer func() { err := os.Unsetenv("REDIS_PASSWORD") if err != nil { diff --git a/pkg/plugin/implementation/cache/cmd/plugin_test.go b/pkg/plugin/implementation/cache/cmd/plugin_test.go index fdc83ff..3ff0a83 100644 --- a/pkg/plugin/implementation/cache/cmd/plugin_test.go +++ b/pkg/plugin/implementation/cache/cmd/plugin_test.go @@ -4,8 +4,12 @@ import ( "context" "os" "testing" + "time" + "github.com/beckn/beckn-onix/pkg/plugin/implementation/cache" + "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) // TestParseConfig tests the configuration parsing logic of the plugin @@ -135,61 +139,149 @@ func TestProviderNew(t *testing.T) { } // TestProviderIntegration tests the provider with a real Redis server -func TestProviderIntegration(t *testing.T) { - // Skip this test if requested - if os.Getenv("SKIP_REDIS_INTEGRATION_TEST") == "true" { - t.Skip("Integration test skipped - SKIP_REDIS_INTEGRATION_TEST=true") - } +// func TestProviderIntegration(t *testing.T) { +// // Skip this test if requested +// if os.Getenv("SKIP_REDIS_INTEGRATION_TEST") == "true" { +// t.Skip("Integration test skipped - SKIP_REDIS_INTEGRATION_TEST=true") +// } - // Set an empty password for testing - 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) - } - }() +// // Set an empty password for testing +// if err := os.Setenv("REDIS_PASSWORD", ""); err != nil { +// t.Fatalf("Failed to set REDIS_PASSWORD: %v", err) +// } - // Create provider and test with real Redis - provider := cacheProvider{} - ctx := context.Background() - config := map[string]string{ - "addr": "localhost:6379", - "db": "0", - } +// // 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) +// } +// }() - cache, cleanup, err := provider.New(ctx, config) - if err != nil { - t.Fatalf("Failed to create cache: %v", err) - } - defer func() { - if err := cleanup(); err != nil { - t.Fatalf("Failed to clean up Redis client: %v", err) - } - }() +// // Create provider and test with real Redis +// provider := cacheProvider{} +// ctx := context.Background() +// config := map[string]string{ +// "addr": "localhost:6379", +// "db": "0", +// } - // Verify it works by setting and getting a value - testKey := "provider_test_key" - testValue := "provider_test_value" +// cache, cleanup, err := provider.New(ctx, config) +// if err != nil { +// t.Fatalf("Failed to create cache: %v", err) +// } +// defer func() { +// if err := cleanup(); err != nil { +// t.Fatalf("Failed to clean up Redis client: %v", err) +// } +// }() - // Set a value - err = cache.Set(ctx, testKey, testValue, 0) - assert.NoError(t, err, "Set operation should not fail") +// // Verify it works by setting and getting a value +// testKey := "provider_test_key" +// testValue := "provider_test_value" - // Get the value - got, err := cache.Get(ctx, testKey) - assert.NoError(t, err, "Get operation should not fail") - assert.Equal(t, testValue, got, "Should get the value that was set") +// // Set a value +// err = cache.Set(ctx, testKey, testValue, 0) +// assert.NoError(t, err, "Set operation should not fail") - // Clean up - err = cache.Delete(ctx, testKey) - assert.NoError(t, err, "Delete operation should not fail") -} +// // Get the value +// got, err := cache.Get(ctx, testKey) +// assert.NoError(t, err, "Get operation should not fail") +// assert.Equal(t, testValue, got, "Should get the value that was set") + +// // Clean up +// err = cache.Delete(ctx, testKey) +// assert.NoError(t, err, "Delete operation should not fail") +// } // TestProviderVariable tests that the Provider variable is correctly initialized func TestProviderVariable(t *testing.T) { assert.NotNil(t, Provider, "Provider should not be nil") } + +// mockRedisClient mocks the RedisClient interface from the cache package +type mockRedisClient struct { + mock.Mock +} + +func (m *mockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd { + args := m.Called(ctx, key) + cmd := redis.NewStringCmd(ctx) + cmd.SetVal(args.String(0)) + return cmd +} + +func (m *mockRedisClient) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.StatusCmd { + args := m.Called(ctx, key, value, ttl) + cmd := redis.NewStatusCmd(ctx) + cmd.SetVal(args.String(0)) + return cmd +} + +func (m *mockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd { + args := m.Called(ctx, keys) + cmd := redis.NewIntCmd(ctx) + cmd.SetVal(int64(args.Int(0))) + return cmd +} + +func (m *mockRedisClient) FlushDB(ctx context.Context) *redis.StatusCmd { + args := m.Called(ctx) + cmd := redis.NewStatusCmd(ctx) + cmd.SetVal(args.String(0)) + return cmd +} + +func (m *mockRedisClient) Ping(ctx context.Context) *redis.StatusCmd { + args := m.Called(ctx) + cmd := redis.NewStatusCmd(ctx) + cmd.SetVal(args.String(0)) + return cmd +} + +func (m *mockRedisClient) Close() error { + args := m.Called() + return args.Error(0) +} + +func TestProviderIntegration(t *testing.T) { + // Save original RedisClientFunc and restore after test + original := cache.RedisClientFunc + defer func() { cache.RedisClientFunc = original }() + + // Create and assign mock + mockClient := new(mockRedisClient) + cache.RedisClientFunc = func(cfg *cache.Config) cache.RedisClient { + return mockClient + } + + ctx := context.Background() + + // Expectations for the mock + mockClient.On("Ping", ctx).Return("PONG") + mockClient.On("Close").Return(nil) + + // Create the config and convert it into a map[string]string + config := &cache.Config{ + Addr: "localhost:35", + } + // Convert the *cache.Config to map[string]string + configMap := map[string]string{ + "addr": config.Addr, + } + + // Call the plugin provider + provider := Provider + c, cleanup, err := provider.New(ctx, configMap) + + // Assertions + assert.NoError(t, err) + assert.NotNil(t, c) + assert.NotNil(t, cleanup) + + // Call cleanup and assert + err = cleanup() + assert.NoError(t, err) + + // Verify expectations + mockClient.AssertExpectations(t) +} From 04ba311628ff6eb02a4136d500c8a8bae9eb2f10 Mon Sep 17 00:00:00 2001 From: Atharva Zade Date: Wed, 14 May 2025 17:00:37 +0530 Subject: [PATCH 6/7] Comment Resolve Redis Plugin --- go.mod | 2 +- pkg/plugin/implementation/cache/cmd/plugin.go | 68 ++------- .../implementation/cache/cmd/plugin_test.go | 132 +----------------- 3 files changed, 13 insertions(+), 189 deletions(-) diff --git a/go.mod b/go.mod index 04f1e61..c00aa40 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/beckn/beckn-onix -go 1.23.0 +go 1.24 toolchain go1.23.4 diff --git a/pkg/plugin/implementation/cache/cmd/plugin.go b/pkg/plugin/implementation/cache/cmd/plugin.go index 07082ab..fda5c97 100644 --- a/pkg/plugin/implementation/cache/cmd/plugin.go +++ b/pkg/plugin/implementation/cache/cmd/plugin.go @@ -3,75 +3,27 @@ package main import ( "context" "errors" - "strconv" "github.com/beckn/beckn-onix/pkg/plugin/definition" "github.com/beckn/beckn-onix/pkg/plugin/implementation/cache" ) -// cacheProvider implements the definition.CacheProvider interface. +// cacheProvider implements the CacheProvider interface for the cache plugin. type cacheProvider struct{} -// Config holds the configuration settings for the Redis cache plugin. -type Config struct { - Addr string // Redis address (host:port) - DB int // Redis database number (optional, defaults to 0) - Password string // Redis password (optional, can be empty or from env) -} - -// parseConfig converts the string map configuration to a Config struct. -func parseConfig(config map[string]string) (*Config, error) { - addr, ok := config["addr"] - if !ok || addr == "" { - return nil, errors.New("config must contain 'addr'") - } - - // Default values - db := 0 - password := "" - - // Parse DB if provided - if val, ok := config["db"]; ok && val != "" { - if parsedVal, err := strconv.Atoi(val); err == nil { - db = parsedVal - } - } - - // Get password if provided - if val, ok := config["password"]; ok { - password = val - } - - return &Config{ - Addr: addr, - DB: db, - Password: password, - }, nil -} - -// convertToRedisConfig converts the plugin Config to redis.Config. -func convertToRedisConfig(cfg *Config) *cache.Config { - return &cache.Config{ - Addr: cfg.Addr, - } -} - -// New initializes a new Redis cache with the given configuration. -func (p cacheProvider) New(ctx context.Context, config map[string]string) (definition.Cache, func() error, error) { +// New creates a new cache plugin instance. +func (c cacheProvider) New(ctx context.Context, config map[string]string) (definition.Cache, func() error, error) { if ctx == nil { return nil, nil, errors.New("context cannot be nil") } - cfg, err := parseConfig(config) - if err != nil { - return nil, nil, err + // Create cache.Config directly from map - validation is handled by cache.New + cacheConfig := &cache.Config{ + Addr: config["addr"], } - - // Convert to redis.Config - redisConfig := convertToRedisConfig(cfg) - - return cache.New(ctx, redisConfig) + + return cache.New(ctx, cacheConfig) } -// Provider is the exported symbol that the plugin manager will look for. -var Provider = cacheProvider{} +// Provider is the exported plugin instance +var Provider definition.CacheProvider = cacheProvider{} \ No newline at end of file diff --git a/pkg/plugin/implementation/cache/cmd/plugin_test.go b/pkg/plugin/implementation/cache/cmd/plugin_test.go index 3ff0a83..f853d63 100644 --- a/pkg/plugin/implementation/cache/cmd/plugin_test.go +++ b/pkg/plugin/implementation/cache/cmd/plugin_test.go @@ -12,79 +12,6 @@ import ( "github.com/stretchr/testify/mock" ) -// TestParseConfig tests the configuration parsing logic of the plugin -func TestParseConfig(t *testing.T) { - tests := []struct { - name string - config map[string]string - want *Config - wantErr bool - }{ - { - name: "missing addr", - config: map[string]string{}, - want: nil, - wantErr: true, - }, - { - name: "empty addr", - config: map[string]string{"addr": ""}, - want: nil, - wantErr: true, - }, - { - name: "basic config", - config: map[string]string{"addr": "localhost:6379"}, - want: &Config{Addr: "localhost:6379", DB: 0, Password: ""}, - wantErr: false, - }, - { - name: "with db", - config: map[string]string{"addr": "localhost:6379", "db": "1"}, - want: &Config{Addr: "localhost:6379", DB: 1, Password: ""}, - wantErr: false, - }, - { - name: "with password", - config: map[string]string{"addr": "localhost:6379", "password": "secret"}, - want: &Config{Addr: "localhost:6379", DB: 0, Password: "secret"}, - wantErr: false, - }, - { - name: "invalid db", - config: map[string]string{"addr": "localhost:6379", "db": "invalid"}, - want: &Config{Addr: "localhost:6379", DB: 0, Password: ""}, - wantErr: false, // Not an error, just defaults to 0 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := parseConfig(tt.config) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - } - }) - } -} - -// TestConvertToRedisConfig tests the configuration conversion logic -func TestConvertToRedisConfig(t *testing.T) { - cfg := &Config{ - Addr: "localhost:6379", - DB: 1, - Password: "secret", - } - - redisConfig := convertToRedisConfig(cfg) - - assert.NotNil(t, redisConfig) - assert.Equal(t, cfg.Addr, redisConfig.Addr) -} - // TestProviderNew tests the New method of the cacheProvider func TestProviderNew(t *testing.T) { provider := cacheProvider{} @@ -138,61 +65,6 @@ func TestProviderNew(t *testing.T) { } } -// TestProviderIntegration tests the provider with a real Redis server -// func TestProviderIntegration(t *testing.T) { -// // Skip this test if requested -// if os.Getenv("SKIP_REDIS_INTEGRATION_TEST") == "true" { -// t.Skip("Integration test skipped - SKIP_REDIS_INTEGRATION_TEST=true") -// } - -// // Set an empty password for testing -// 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{} -// ctx := context.Background() -// config := map[string]string{ -// "addr": "localhost:6379", -// "db": "0", -// } - -// cache, cleanup, err := provider.New(ctx, config) -// if err != nil { -// t.Fatalf("Failed to create cache: %v", err) -// } -// 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" -// testValue := "provider_test_value" - -// // Set a value -// err = cache.Set(ctx, testKey, testValue, 0) -// assert.NoError(t, err, "Set operation should not fail") - -// // Get the value -// got, err := cache.Get(ctx, testKey) -// assert.NoError(t, err, "Get operation should not fail") -// assert.Equal(t, testValue, got, "Should get the value that was set") - -// // Clean up -// err = cache.Delete(ctx, testKey) -// assert.NoError(t, err, "Delete operation should not fail") -// } - // TestProviderVariable tests that the Provider variable is correctly initialized func TestProviderVariable(t *testing.T) { assert.NotNil(t, Provider, "Provider should not be nil") @@ -262,7 +134,7 @@ func TestProviderIntegration(t *testing.T) { // Create the config and convert it into a map[string]string config := &cache.Config{ - Addr: "localhost:35", + Addr: "localhost:6379", } // Convert the *cache.Config to map[string]string configMap := map[string]string{ @@ -284,4 +156,4 @@ func TestProviderIntegration(t *testing.T) { // Verify expectations mockClient.AssertExpectations(t) -} +} \ No newline at end of file From 56f804c0ca894ba463fc78b4249ab37c7f7d08d2 Mon Sep 17 00:00:00 2001 From: MohitKatare-protean Date: Wed, 28 May 2025 17:12:09 +0530 Subject: [PATCH 7/7] Added logs, comments and issue fix Fixed issue for the interface not implemented for the provider while loading the plugin --- pkg/plugin/implementation/cache/cache.go | 17 ++- pkg/plugin/implementation/cache/cmd/plugin.go | 15 ++- test.go | 103 ++++++++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 test.go diff --git a/pkg/plugin/implementation/cache/cache.go b/pkg/plugin/implementation/cache/cache.go index c8c374a..7007492 100644 --- a/pkg/plugin/implementation/cache/cache.go +++ b/pkg/plugin/implementation/cache/cache.go @@ -7,10 +7,11 @@ import ( "os" "time" + "github.com/beckn/beckn-onix/pkg/log" "github.com/redis/go-redis/v9" ) -// Global variable for the Redis client, can be overridden in tests +// RedisCl global variable for the Redis client, can be overridden in tests var RedisCl *redis.Client // RedisClient is an interface for Redis operations that allows mocking @@ -23,14 +24,17 @@ type RedisClient interface { Close() error } +// Config holds the configuration required to connect to Redis. type Config struct { Addr string } +// Cache wraps a Redis client to provide basic caching operations. type Cache struct { Client RedisClient } +// Error variables to describe common failure modes. var ( ErrEmptyConfig = errors.New("empty config") ErrAddrMissing = errors.New("missing required field 'Addr'") @@ -38,6 +42,7 @@ var ( ErrConnectionFail = errors.New("failed to connect to Redis") ) +// validate checks if the provided Redis configuration is valid. func validate(cfg *Config) error { if cfg == nil { return ErrEmptyConfig @@ -48,6 +53,8 @@ func validate(cfg *Config) error { return nil } +// RedisClientFunc is a function variable that creates a Redis client based on the provided configuration. +// It can be overridden for testing purposes. var RedisClientFunc = func(cfg *Config) RedisClient { return redis.NewClient(&redis.Options{ Addr: cfg.Addr, @@ -56,7 +63,9 @@ var RedisClientFunc = func(cfg *Config) RedisClient { }) } +// New initializes and returns a Cache instance along with a close function to release resources. func New(ctx context.Context, cfg *Config) (*Cache, func() error, error) { + log.Debugf(ctx, "Initializing Cache with config: %+v", cfg) if err := validate(cfg); err != nil { return nil, nil, err } @@ -64,24 +73,30 @@ func New(ctx context.Context, cfg *Config) (*Cache, func() error, error) { client := RedisClientFunc(cfg) if _, err := client.Ping(ctx).Result(); err != nil { + log.Errorf(ctx, err, "Failed to ping Redis server") return nil, nil, fmt.Errorf("%w: %v", ErrConnectionFail, err) } + log.Infof(ctx, "Cache connection to Redis established successfully") return &Cache{Client: client}, client.Close, nil } +// Get retrieves the value for the specified key from Redis. func (c *Cache) Get(ctx context.Context, key string) (string, error) { return c.Client.Get(ctx, key).Result() } +// Set stores the given key-value pair in Redis with the specified TTL (time to live). func (c *Cache) Set(ctx context.Context, key, value string, ttl time.Duration) error { return c.Client.Set(ctx, key, value, ttl).Err() } +// Delete removes the specified key from Redis. func (c *Cache) Delete(ctx context.Context, key string) error { return c.Client.Del(ctx, key).Err() } +// Clear removes all keys in the currently selected Redis database. func (c *Cache) Clear(ctx context.Context) error { return c.Client.FlushDB(ctx).Err() } diff --git a/pkg/plugin/implementation/cache/cmd/plugin.go b/pkg/plugin/implementation/cache/cmd/plugin.go index fda5c97..4cd5869 100644 --- a/pkg/plugin/implementation/cache/cmd/plugin.go +++ b/pkg/plugin/implementation/cache/cmd/plugin.go @@ -4,6 +4,7 @@ import ( "context" "errors" + "github.com/beckn/beckn-onix/pkg/log" "github.com/beckn/beckn-onix/pkg/plugin/definition" "github.com/beckn/beckn-onix/pkg/plugin/implementation/cache" ) @@ -16,14 +17,20 @@ func (c cacheProvider) New(ctx context.Context, config map[string]string) (defin if ctx == nil { return nil, nil, errors.New("context cannot be nil") } - // Create cache.Config directly from map - validation is handled by cache.New cacheConfig := &cache.Config{ Addr: config["addr"], } - - return cache.New(ctx, cacheConfig) + log.Debugf(ctx, "Cache config mapped: %+v", cacheConfig) + cache, closer, err := cache.New(ctx, cacheConfig) + if err != nil { + log.Errorf(ctx, err, "Failed to create cache instance") + return nil, nil, err + } + + log.Infof(ctx, "Cache instance created successfully") + return cache, closer, nil } // Provider is the exported plugin instance -var Provider definition.CacheProvider = cacheProvider{} \ No newline at end of file +var Provider = cacheProvider{} diff --git a/test.go b/test.go new file mode 100644 index 0000000..3a82846 --- /dev/null +++ b/test.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "fmt" + "plugin" + "time" + + "github.com/beckn/beckn-onix/pkg/plugin/definition" +) + +func main() { + ctx := context.Background() + + // Path to the compiled plugin .so file + // Adjust the path accordingly + pluginPath := "pkg/plugin/implementation/cache.so" + + // Open the plugin + p, err := plugin.Open(pluginPath) + if err != nil { + fmt.Printf("Failed to open plugin: %v\n", err) + return + } + + // Lookup the 'Provider' symbol + symProvider, err := p.Lookup("Provider") + if err != nil { + fmt.Printf("Failed to lookup 'Provider': %v\n", err) + return + } + + // Assert that the symbol implements the CacheProvider interface + provider, ok := symProvider.(definition.CacheProvider) + if !ok { + fmt.Println("Plugin 'Provider' does not implement CacheProvider interface.") + return + } + fmt.Println("Successfully loaded CacheProvider plugin.") + + // Setup config + config := map[string]string{ + "addr": "localhost:6379", // Adjust to your Redis instance + } + + // Create a new cache instance using the plugin provider + cacheInstance, cleanup, err := provider.New(ctx, config) + if err != nil { + fmt.Printf("Error creating cache instance: %v\n", err) + return + } + defer func() { + if cleanup != nil { + _ = cleanup() + } + }() + + fmt.Println("Cache instance created successfully.") + + // Test Set + key := "plugin_test_key" + value := "plugin_test_value" + ttl := 10 * time.Second + + err = cacheInstance.Set(ctx, key, value, ttl) + if err != nil { + fmt.Printf("Set failed: %v\n", err) + return + } + fmt.Println("Set operation successful.") + + // Test Get + got, err := cacheInstance.Get(ctx, key) + if err != nil { + fmt.Printf("Get failed: %v\n", err) + return + } + fmt.Printf("Got value: %s\n", got) + + // Test Delete + err = cacheInstance.Delete(ctx, key) + if err != nil { + fmt.Printf("Delete failed: %v\n", err) + return + } + fmt.Println("Delete operation successful.") + + // Test Clear + // Add a key to test Clear + err = cacheInstance.Set(ctx, "another_plugin_key", "another_plugin_value", ttl) + if err != nil { + fmt.Printf("Set for clear test failed: %v\n", err) + return + } + fmt.Println("Added key for clear test.") + + err = cacheInstance.Clear(ctx) + if err != nil { + fmt.Printf("Clear failed: %v\n", err) + return + } + fmt.Println("Clear operation successful.") +}