Merge pull request #471 from beckn/feature/redis-plugin

Initial commit for Cache Plugin
This commit is contained in:
Tanya Madaan
2025-06-16 12:42:45 +05:30
committed by GitHub
7 changed files with 616 additions and 1 deletions

5
go.mod
View File

@@ -2,6 +2,8 @@ module github.com/beckn/beckn-onix
go 1.24
toolchain go1.23.4
require (
github.com/kr/pretty v0.3.1 // indirect
github.com/rogpeppe/go-internal v1.13.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/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
@@ -48,6 +52,7 @@ require (
require (
github.com/google/uuid v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/redis/go-redis/v9 v9.8.0
github.com/hashicorp/vault/api v1.16.0
github.com/rabbitmq/amqp091-go v1.10.0
github.com/rs/zerolog v1.34.0

12
go.sum
View File

@@ -1,3 +1,9 @@
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/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -6,6 +12,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
@@ -72,6 +80,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -121,4 +131,4 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

102
pkg/plugin/implementation/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,102 @@
package cache
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/beckn/beckn-onix/pkg/log"
"github.com/redis/go-redis/v9"
)
// 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
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
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'")
ErrCredentialMissing = errors.New("missing Redis credentials in environment")
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
}
if cfg.Addr == "" {
return ErrAddrMissing
}
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,
Password: os.Getenv("REDIS_PASSWORD"),
DB: 0,
})
}
// 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
}
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()
}

View File

@@ -0,0 +1,200 @@
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 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)
}
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}
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
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)
if tt.wantErr != nil {
assert.Equal(t, tt.wantErr, err)
} else {
assert.NoError(t, err)
}
})
}
}
// 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,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, _, err := New(context.Background(), tc.cfg)
if tc.wantErr {
assert.Error(t, err)
if tc.errType != nil {
assert.ErrorIs(t, err, tc.errType)
}
} else {
assert.NoError(t, err)
}
})
}
}
// 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 set REDIS_PASSWORD environment variable: %v", err)
}
defer func() {
err := os.Unsetenv("REDIS_PASSWORD")
if err != nil {
t.Fatalf("Failed to unset REDIS_PASSWORD environment variable: %v", err)
}
}()
// Use an invalid connection address to force a connection failure
cfg := &Config{Addr: "invalid:1234"}
// Call New which should fail with a connection error
_, _, err = New(context.Background(), cfg)
// Verify error type is connection failure
assert.Error(t, err)
assert.ErrorIs(t, err, ErrConnectionFail)
}

View File

@@ -0,0 +1,36 @@
package main
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"
)
// cacheProvider implements the CacheProvider interface for the cache plugin.
type cacheProvider struct{}
// 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")
}
// Create cache.Config directly from map - validation is handled by cache.New
cacheConfig := &cache.Config{
Addr: config["addr"],
}
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 = cacheProvider{}

View File

@@ -0,0 +1,159 @@
package main
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"
)
// 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 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
if err := os.Setenv("REDIS_PASSWORD", ""); err != nil {
t.Fatalf("Failed to set REDIS_PASSWORD: %v", err)
}
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)
}
})
}
}
// 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:6379",
}
// 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)
}

103
test.go Normal file
View File

@@ -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.")
}