diff --git a/.github/actions/run-integration-tests/action.yml b/.github/actions/run-integration-tests/action.yml index 85317a293..92f194a5a 100644 --- a/.github/actions/run-integration-tests/action.yml +++ b/.github/actions/run-integration-tests/action.yml @@ -1,14 +1,12 @@ name: Run Integration Tests -description: Runs integration tests against a specific database type +description: Runs integration tests against a specific database type (sqlite, postgres, or redis) inputs: database-type: - description: 'Database type to test against (sqlite or postgres)' + description: 'Database type to test against (sqlite, postgres, or redis)' required: true - type: string coverage-enabled: description: 'Whether to enable coverage collection' required: false - type: boolean default: false runs: diff --git a/.github/workflows/pr-builder.yml b/.github/workflows/pr-builder.yml index 0f163ab61..c4d83d32a 100644 --- a/.github/workflows/pr-builder.yml +++ b/.github/workflows/pr-builder.yml @@ -474,7 +474,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - database: [sqlite, postgres] + database: [sqlite, postgres, redis] fail-fast: false services: postgres: @@ -490,6 +490,15 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis:latest + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: 📥 Checkout Code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 diff --git a/.vale/styles/config/vocabularies/vocab/accept.txt b/.vale/styles/config/vocabularies/vocab/accept.txt index 2749898b0..b3e0e3094 100644 --- a/.vale/styles/config/vocabularies/vocab/accept.txt +++ b/.vale/styles/config/vocabularies/vocab/accept.txt @@ -50,3 +50,5 @@ templated ======= JWTs >>>>>>> 5337c408 (Add initial quickstarts and guides to Thunder) +ACL +ACLs \ No newline at end of file diff --git a/backend/cmd/server/repository/conf/deployment.yaml b/backend/cmd/server/repository/conf/deployment.yaml index 6dd8c14e7..2c9a7c00c 100644 --- a/backend/cmd/server/repository/conf/deployment.yaml +++ b/backend/cmd/server/repository/conf/deployment.yaml @@ -10,27 +10,30 @@ tls: database: config: type: "sqlite" - path: "repository/database/configdb.db" - options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" - max_open_conns: 500 - max_idle_conns: 100 - conn_max_lifetime: 3600 + sqlite: + path: "repository/database/configdb.db" + options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 runtime: type: "sqlite" - path: "repository/database/runtimedb.db" - options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" - max_open_conns: 500 - max_idle_conns: 100 - conn_max_lifetime: 3600 + sqlite: + path: "repository/database/runtimedb.db" + options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 user: type: "sqlite" - path: "repository/database/userdb.db" - options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" - max_open_conns: 500 - max_idle_conns: 100 - conn_max_lifetime: 3600 + sqlite: + path: "repository/database/userdb.db" + options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 crypto: encryption: diff --git a/backend/cmd/server/repository/resources/conf/default.json b/backend/cmd/server/repository/resources/conf/default.json index e15a569c4..ac69a1e34 100644 --- a/backend/cmd/server/repository/resources/conf/default.json +++ b/backend/cmd/server/repository/resources/conf/default.json @@ -19,45 +19,40 @@ "database": { "config": { "type": "sqlite", - "hostname": "", - "port": 0, - "name": "", - "username": "", - "password": "", - "sslmode": "", - "path": "repository/database/configdb.db", - "options": "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)", - "max_open_conns": 500, - "max_idle_conns": 100, - "conn_max_lifetime": 3600 + "sqlite": { + "path": "repository/database/configdb.db", + "options": "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)", + "max_open_conns": 500, + "max_idle_conns": 100, + "conn_max_lifetime": 3600 + } }, "runtime": { "type": "sqlite", - "hostname": "", - "port": 0, - "name": "", - "username": "", - "password": "", - "sslmode": "", - "path": "repository/database/runtimedb.db", - "options": "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)", - "max_open_conns": 500, - "max_idle_conns": 100, - "conn_max_lifetime": 3600 + "sqlite": { + "path": "repository/database/runtimedb.db", + "options": "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)", + "max_open_conns": 500, + "max_idle_conns": 100, + "conn_max_lifetime": 3600 + }, + "redis": { + "address": "", + "username": "", + "password": "", + "db": 0, + "key_prefix": "" + } }, "user": { "type": "sqlite", - "hostname": "", - "port": 0, - "name": "", - "username": "", - "password": "", - "sslmode": "", - "path": "repository/database/userdb.db", - "options": "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)", - "max_open_conns": 500, - "max_idle_conns": 100, - "conn_max_lifetime": 3600 + "sqlite": { + "path": "repository/database/userdb.db", + "options": "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)", + "max_open_conns": 500, + "max_idle_conns": 100, + "conn_max_lifetime": 3600 + } } }, "cache": { diff --git a/backend/internal/application/init_test.go b/backend/internal/application/init_test.go index 861885df9..d79273192 100644 --- a/backend/internal/application/init_test.go +++ b/backend/internal/application/init_test.go @@ -48,10 +48,8 @@ import ( // process share the same SQLite instance instead of creating separate databases. func newInMemoryDataSource() config.DataSource { return config.DataSource{ - Type: "sqlite", - Path: "file::memory:?cache=shared", - MaxOpenConns: 1, - MaxIdleConns: 1, + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "file::memory:?cache=shared", MaxOpenConns: 1, MaxIdleConns: 1}, } } diff --git a/backend/internal/attributecache/init.go b/backend/internal/attributecache/init.go index 6c4b042f8..2b8d3c8b9 100644 --- a/backend/internal/attributecache/init.go +++ b/backend/internal/attributecache/init.go @@ -18,9 +18,18 @@ package attributecache -// Initialize initializes the attribute cache service with a SQL store -// and returns an instance of AttributeCacheServiceInterface. +import ( + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/internal/system/database/provider" +) + +// Initialize initializes the attribute cache service and returns an instance of AttributeCacheServiceInterface. func Initialize() AttributeCacheServiceInterface { - store := newAttributeCacheStore() + var store attributeCacheStoreInterface + if config.GetThunderRuntime().Config.Database.Runtime.Type == provider.DataSourceTypeRedis { + store = newRedisAttributeCacheStore(provider.GetRedisProvider()) + } else { + store = newAttributeCacheStore() + } return newAttributeCacheService(store) } diff --git a/backend/internal/attributecache/redisClient_mock_test.go b/backend/internal/attributecache/redisClient_mock_test.go new file mode 100644 index 000000000..4964989c5 --- /dev/null +++ b/backend/internal/attributecache/redisClient_mock_test.go @@ -0,0 +1,366 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package attributecache + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// newRedisClientMock creates a new instance of redisClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newRedisClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *redisClientMock { + mock := &redisClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// redisClientMock is an autogenerated mock type for the redisClient type +type redisClientMock struct { + mock.Mock +} + +type redisClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *redisClientMock) EXPECT() *redisClientMock_Expecter { + return &redisClientMock_Expecter{mock: &_m.Mock} +} + +// Del provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Del(ctx context.Context, keys ...string) *redis.IntCmd { + // string + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Del") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// redisClientMock_Del_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Del' +type redisClientMock_Del_Call struct { + *mock.Call +} + +// Del is a helper method to define mock.On call +// - ctx context.Context +// - keys ...string +func (_e *redisClientMock_Expecter) Del(ctx interface{}, keys ...interface{}) *redisClientMock_Del_Call { + return &redisClientMock_Del_Call{Call: _e.mock.On("Del", + append([]interface{}{ctx}, keys...)...)} +} + +func (_c *redisClientMock_Del_Call) Run(run func(ctx context.Context, keys ...string)) *redisClientMock_Del_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *redisClientMock_Del_Call) Return(intCmd *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *redisClientMock_Del_Call) RunAndReturn(run func(ctx context.Context, keys ...string) *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(run) + return _c +} + +// Expire provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd { + ret := _mock.Called(ctx, key, expiration) + + if len(ret) == 0 { + panic("no return value specified for Expire") + } + + var r0 *redis.BoolCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Duration) *redis.BoolCmd); ok { + r0 = returnFunc(ctx, key, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.BoolCmd) + } + } + return r0 +} + +// redisClientMock_Expire_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Expire' +type redisClientMock_Expire_Call struct { + *mock.Call +} + +// Expire is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - expiration time.Duration +func (_e *redisClientMock_Expecter) Expire(ctx interface{}, key interface{}, expiration interface{}) *redisClientMock_Expire_Call { + return &redisClientMock_Expire_Call{Call: _e.mock.On("Expire", ctx, key, expiration)} +} + +func (_c *redisClientMock_Expire_Call) Run(run func(ctx context.Context, key string, expiration time.Duration)) *redisClientMock_Expire_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 time.Duration + if args[2] != nil { + arg2 = args[2].(time.Duration) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *redisClientMock_Expire_Call) Return(boolCmd *redis.BoolCmd) *redisClientMock_Expire_Call { + _c.Call.Return(boolCmd) + return _c +} + +func (_c *redisClientMock_Expire_Call) RunAndReturn(run func(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd) *redisClientMock_Expire_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Get(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// redisClientMock_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type redisClientMock_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *redisClientMock_Expecter) Get(ctx interface{}, key interface{}) *redisClientMock_Get_Call { + return &redisClientMock_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *redisClientMock_Get_Call) Run(run func(ctx context.Context, key string)) *redisClientMock_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_Get_Call) Return(stringCmd *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *redisClientMock_Get_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + ret := _mock.Called(ctx, key, value, expiration) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, interface{}, time.Duration) *redis.StatusCmd); ok { + r0 = returnFunc(ctx, key, value, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// redisClientMock_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type redisClientMock_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value interface{} +// - expiration time.Duration +func (_e *redisClientMock_Expecter) Set(ctx interface{}, key interface{}, value interface{}, expiration interface{}) *redisClientMock_Set_Call { + return &redisClientMock_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expiration)} +} + +func (_c *redisClientMock_Set_Call) Run(run func(ctx context.Context, key string, value interface{}, expiration time.Duration)) *redisClientMock_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 interface{} + if args[2] != nil { + arg2 = args[2].(interface{}) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *redisClientMock_Set_Call) Return(statusCmd *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *redisClientMock_Set_Call) RunAndReturn(run func(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(run) + return _c +} + +// TTL provides a mock function for the type redisClientMock +func (_mock *redisClientMock) TTL(ctx context.Context, key string) *redis.DurationCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for TTL") + } + + var r0 *redis.DurationCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.DurationCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.DurationCmd) + } + } + return r0 +} + +// redisClientMock_TTL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TTL' +type redisClientMock_TTL_Call struct { + *mock.Call +} + +// TTL is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *redisClientMock_Expecter) TTL(ctx interface{}, key interface{}) *redisClientMock_TTL_Call { + return &redisClientMock_TTL_Call{Call: _e.mock.On("TTL", ctx, key)} +} + +func (_c *redisClientMock_TTL_Call) Run(run func(ctx context.Context, key string)) *redisClientMock_TTL_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_TTL_Call) Return(durationCmd *redis.DurationCmd) *redisClientMock_TTL_Call { + _c.Call.Return(durationCmd) + return _c +} + +func (_c *redisClientMock_TTL_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.DurationCmd) *redisClientMock_TTL_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/internal/attributecache/redis_store.go b/backend/internal/attributecache/redis_store.go new file mode 100644 index 000000000..2ce89c431 --- /dev/null +++ b/backend/internal/attributecache/redis_store.go @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package attributecache + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/internal/system/database/provider" +) + +// redisClient abstracts the Redis commands used by the attribute cache store. +type redisClient interface { + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd + Get(ctx context.Context, key string) *redis.StringCmd + TTL(ctx context.Context, key string) *redis.DurationCmd + Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd + Del(ctx context.Context, keys ...string) *redis.IntCmd +} + +// redisAttributeCacheStore is the Redis-backed implementation of attributeCacheStoreInterface. +type redisAttributeCacheStore struct { + client redisClient + keyPrefix string + deploymentID string +} + +// newRedisAttributeCacheStore creates a new Redis-backed attribute cache store. +func newRedisAttributeCacheStore(p provider.RedisProviderInterface) attributeCacheStoreInterface { + return &redisAttributeCacheStore{ + client: p.GetRedisClient(), + keyPrefix: p.GetKeyPrefix(), + deploymentID: config.GetThunderRuntime().Config.Server.Identifier, + } +} + +// cacheKey builds the Redis key for an attribute cache entry. +func (s *redisAttributeCacheStore) cacheKey(id string) string { + return fmt.Sprintf("%s:runtime:%s:attrcache:%s", s.keyPrefix, s.deploymentID, id) +} + +// CreateAttributeCache serializes the attribute cache entry and stores it in Redis with a TTL. +func (s *redisAttributeCacheStore) CreateAttributeCache(ctx context.Context, cache AttributeCache) error { + data, err := json.Marshal(cache) + if err != nil { + return fmt.Errorf("failed to marshal attribute cache: %w", err) + } + + ttl := time.Duration(cache.TTLSeconds) * time.Second + if err := s.client.Set(ctx, s.cacheKey(cache.ID), data, ttl).Err(); err != nil { + return fmt.Errorf("failed to store attribute cache in Redis: %w", err) + } + + return nil +} + +// GetAttributeCache retrieves an attribute cache entry from Redis. +func (s *redisAttributeCacheStore) GetAttributeCache(ctx context.Context, id string) (AttributeCache, error) { + key := s.cacheKey(id) + + data, err := s.client.Get(ctx, key).Bytes() + if err != nil { + if errors.Is(err, redis.Nil) { + return AttributeCache{}, errAttributeCacheNotFound + } + return AttributeCache{}, fmt.Errorf("failed to get attribute cache from Redis: %w", err) + } + + var result AttributeCache + if err := json.Unmarshal(data, &result); err != nil { + return AttributeCache{}, fmt.Errorf("failed to unmarshal attribute cache: %w", err) + } + + // Reflect the actual remaining TTL from Redis. + ttl, err := s.client.TTL(ctx, key).Result() + if err == nil && ttl > 0 { + result.TTLSeconds = int(ttl.Seconds()) + } + + return result, nil +} + +// ExtendAttributeCacheTTL extends the TTL of an attribute cache entry in Redis. +func (s *redisAttributeCacheStore) ExtendAttributeCacheTTL(ctx context.Context, id string, ttlSeconds int) error { + ttl := time.Duration(ttlSeconds) * time.Second + ok, err := s.client.Expire(ctx, s.cacheKey(id), ttl).Result() + if err != nil { + return fmt.Errorf("failed to extend attribute cache TTL in Redis: %w", err) + } + if !ok { + return errAttributeCacheNotFound + } + + return nil +} + +// DeleteAttributeCache removes an attribute cache entry from Redis. +func (s *redisAttributeCacheStore) DeleteAttributeCache(ctx context.Context, id string) error { + n, err := s.client.Del(ctx, s.cacheKey(id)).Result() + if err != nil { + return fmt.Errorf("failed to delete attribute cache from Redis: %w", err) + } + if n == 0 { + return errAttributeCacheNotFound + } + + return nil +} diff --git a/backend/internal/attributecache/redis_store_test.go b/backend/internal/attributecache/redis_store_test.go new file mode 100644 index 000000000..bf77474f1 --- /dev/null +++ b/backend/internal/attributecache/redis_store_test.go @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package attributecache + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +const ( + redisTestKeyPrefix = "thunder" + redisTestDeploymentID = "test-deployment" + redisTestCacheID = "test-cache-id" +) + +type RedisAttributeCacheStoreTestSuite struct { + suite.Suite + store *redisAttributeCacheStore + mockClient *redisClientMock + ctx context.Context + testCache AttributeCache + cacheKey string +} + +func TestRedisAttributeCacheStoreSuite(t *testing.T) { + suite.Run(t, new(RedisAttributeCacheStoreTestSuite)) +} + +func (suite *RedisAttributeCacheStoreTestSuite) SetupTest() { + suite.mockClient = newRedisClientMock(suite.T()) + suite.ctx = context.Background() + suite.testCache = AttributeCache{ + ID: redisTestCacheID, + Attributes: map[string]interface{}{"key": "value"}, + TTLSeconds: 3600, + } + suite.store = &redisAttributeCacheStore{ + client: suite.mockClient, + keyPrefix: redisTestKeyPrefix, + deploymentID: redisTestDeploymentID, + } + suite.cacheKey = fmt.Sprintf("%s:runtime:%s:attrcache:%s", + redisTestKeyPrefix, redisTestDeploymentID, redisTestCacheID) +} + +// Tests for cacheKey + +func (suite *RedisAttributeCacheStoreTestSuite) TestCacheKey() { + key := suite.store.cacheKey(redisTestCacheID) + suite.Equal(suite.cacheKey, key) +} + +// Tests for CreateAttributeCache + +func (suite *RedisAttributeCacheStoreTestSuite) TestCreateAttributeCache_Success() { + statusCmd := redis.NewStatusCmd(suite.ctx) + suite.mockClient.On("Set", suite.ctx, suite.cacheKey, mock.Anything, + time.Duration(suite.testCache.TTLSeconds)*time.Second).Return(statusCmd) + + err := suite.store.CreateAttributeCache(suite.ctx, suite.testCache) + suite.NoError(err) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestCreateAttributeCache_SetError() { + statusCmd := redis.NewStatusCmd(suite.ctx) + statusCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Set", suite.ctx, suite.cacheKey, mock.Anything, + time.Duration(suite.testCache.TTLSeconds)*time.Second).Return(statusCmd) + + err := suite.store.CreateAttributeCache(suite.ctx, suite.testCache) + suite.Error(err) + suite.Contains(err.Error(), "failed to store attribute cache in Redis") +} + +// Tests for GetAttributeCache + +func (suite *RedisAttributeCacheStoreTestSuite) TestGetAttributeCache_Success() { + data, _ := json.Marshal(suite.testCache) + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal(string(data)) + suite.mockClient.On("Get", suite.ctx, suite.cacheKey).Return(stringCmd) + + durationCmd := redis.NewDurationCmd(suite.ctx, time.Second) + durationCmd.SetVal(30 * time.Minute) + suite.mockClient.On("TTL", suite.ctx, suite.cacheKey).Return(durationCmd) + + result, err := suite.store.GetAttributeCache(suite.ctx, redisTestCacheID) + suite.NoError(err) + suite.Equal(redisTestCacheID, result.ID) + suite.Equal(1800, result.TTLSeconds) // Overridden by Redis TTL (30 min = 1800 s) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestGetAttributeCache_NotFound() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetErr(redis.Nil) + suite.mockClient.On("Get", suite.ctx, suite.cacheKey).Return(stringCmd) + + result, err := suite.store.GetAttributeCache(suite.ctx, redisTestCacheID) + suite.Error(err) + suite.Equal(errAttributeCacheNotFound, err) + suite.Equal(AttributeCache{}, result) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestGetAttributeCache_GetError() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Get", suite.ctx, suite.cacheKey).Return(stringCmd) + + result, err := suite.store.GetAttributeCache(suite.ctx, redisTestCacheID) + suite.Error(err) + suite.Contains(err.Error(), "failed to get attribute cache from Redis") + suite.Equal(AttributeCache{}, result) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestGetAttributeCache_UnmarshalError() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal("not valid json{{{") + suite.mockClient.On("Get", suite.ctx, suite.cacheKey).Return(stringCmd) + + result, err := suite.store.GetAttributeCache(suite.ctx, redisTestCacheID) + suite.Error(err) + suite.Contains(err.Error(), "failed to unmarshal attribute cache") + suite.Equal(AttributeCache{}, result) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestGetAttributeCache_TTLError_KeepsStoredTTL() { + // When TTL call fails, the stored TTLSeconds from JSON is kept. + data, _ := json.Marshal(suite.testCache) + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal(string(data)) + suite.mockClient.On("Get", suite.ctx, suite.cacheKey).Return(stringCmd) + + durationCmd := redis.NewDurationCmd(suite.ctx, time.Second) + durationCmd.SetErr(errors.New("redis error")) + suite.mockClient.On("TTL", suite.ctx, suite.cacheKey).Return(durationCmd) + + result, err := suite.store.GetAttributeCache(suite.ctx, redisTestCacheID) + suite.NoError(err) + suite.Equal(suite.testCache.TTLSeconds, result.TTLSeconds) // Falls back to stored value +} + +// Tests for ExtendAttributeCacheTTL + +func (suite *RedisAttributeCacheStoreTestSuite) TestExtendAttributeCacheTTL_Success() { + newTTL := 7200 + boolCmd := redis.NewBoolCmd(suite.ctx) + boolCmd.SetVal(true) + suite.mockClient.On("Expire", suite.ctx, suite.cacheKey, + time.Duration(newTTL)*time.Second).Return(boolCmd) + + err := suite.store.ExtendAttributeCacheTTL(suite.ctx, redisTestCacheID, newTTL) + suite.NoError(err) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestExtendAttributeCacheTTL_NotFound() { + newTTL := 7200 + boolCmd := redis.NewBoolCmd(suite.ctx) + boolCmd.SetVal(false) // Key not found + suite.mockClient.On("Expire", suite.ctx, suite.cacheKey, + time.Duration(newTTL)*time.Second).Return(boolCmd) + + err := suite.store.ExtendAttributeCacheTTL(suite.ctx, redisTestCacheID, newTTL) + suite.Error(err) + suite.Equal(errAttributeCacheNotFound, err) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestExtendAttributeCacheTTL_ExpireError() { + newTTL := 7200 + boolCmd := redis.NewBoolCmd(suite.ctx) + boolCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Expire", suite.ctx, suite.cacheKey, + time.Duration(newTTL)*time.Second).Return(boolCmd) + + err := suite.store.ExtendAttributeCacheTTL(suite.ctx, redisTestCacheID, newTTL) + suite.Error(err) + suite.Contains(err.Error(), "failed to extend attribute cache TTL in Redis") +} + +// Tests for DeleteAttributeCache + +func (suite *RedisAttributeCacheStoreTestSuite) TestDeleteAttributeCache_Success() { + intCmd := redis.NewIntCmd(suite.ctx) + intCmd.SetVal(1) + suite.mockClient.On("Del", suite.ctx, suite.cacheKey).Return(intCmd) + + err := suite.store.DeleteAttributeCache(suite.ctx, redisTestCacheID) + suite.NoError(err) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestDeleteAttributeCache_NotFound() { + intCmd := redis.NewIntCmd(suite.ctx) + intCmd.SetVal(0) // Key not found + suite.mockClient.On("Del", suite.ctx, suite.cacheKey).Return(intCmd) + + err := suite.store.DeleteAttributeCache(suite.ctx, redisTestCacheID) + suite.Error(err) + suite.Equal(errAttributeCacheNotFound, err) +} + +func (suite *RedisAttributeCacheStoreTestSuite) TestDeleteAttributeCache_DelError() { + intCmd := redis.NewIntCmd(suite.ctx) + intCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Del", suite.ctx, suite.cacheKey).Return(intCmd) + + err := suite.store.DeleteAttributeCache(suite.ctx, redisTestCacheID) + suite.Error(err) + suite.Contains(err.Error(), "failed to delete attribute cache from Redis") +} diff --git a/backend/internal/authn/passkey/init.go b/backend/internal/authn/passkey/init.go index 42284ff07..34ec65f58 100644 --- a/backend/internal/authn/passkey/init.go +++ b/backend/internal/authn/passkey/init.go @@ -19,13 +19,19 @@ package passkey import ( + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/internal/system/database/provider" "github.com/asgardeo/thunder/internal/user" ) // Initialize initializes the WebAuthn authentication service. func Initialize(userSvc user.UserServiceInterface) PasskeyServiceInterface { - // Create the session store - sessionStore := newSessionStore() + var store sessionStoreInterface + if config.GetThunderRuntime().Config.Database.Runtime.Type == provider.DataSourceTypeRedis { + store = newRedisSessionStore(provider.GetRedisProvider()) + } else { + store = newSessionStore() + } - return newPasskeyService(userSvc, sessionStore) + return newPasskeyService(userSvc, store) } diff --git a/backend/internal/authn/passkey/redisClient_mock_test.go b/backend/internal/authn/passkey/redisClient_mock_test.go new file mode 100644 index 000000000..33c29882b --- /dev/null +++ b/backend/internal/authn/passkey/redisClient_mock_test.go @@ -0,0 +1,242 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package passkey + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// newRedisClientMock creates a new instance of redisClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newRedisClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *redisClientMock { + mock := &redisClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// redisClientMock is an autogenerated mock type for the redisClient type +type redisClientMock struct { + mock.Mock +} + +type redisClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *redisClientMock) EXPECT() *redisClientMock_Expecter { + return &redisClientMock_Expecter{mock: &_m.Mock} +} + +// Del provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Del(ctx context.Context, keys ...string) *redis.IntCmd { + // string + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Del") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// redisClientMock_Del_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Del' +type redisClientMock_Del_Call struct { + *mock.Call +} + +// Del is a helper method to define mock.On call +// - ctx context.Context +// - keys ...string +func (_e *redisClientMock_Expecter) Del(ctx interface{}, keys ...interface{}) *redisClientMock_Del_Call { + return &redisClientMock_Del_Call{Call: _e.mock.On("Del", + append([]interface{}{ctx}, keys...)...)} +} + +func (_c *redisClientMock_Del_Call) Run(run func(ctx context.Context, keys ...string)) *redisClientMock_Del_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *redisClientMock_Del_Call) Return(intCmd *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *redisClientMock_Del_Call) RunAndReturn(run func(ctx context.Context, keys ...string) *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Get(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// redisClientMock_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type redisClientMock_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *redisClientMock_Expecter) Get(ctx interface{}, key interface{}) *redisClientMock_Get_Call { + return &redisClientMock_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *redisClientMock_Get_Call) Run(run func(ctx context.Context, key string)) *redisClientMock_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_Get_Call) Return(stringCmd *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *redisClientMock_Get_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + ret := _mock.Called(ctx, key, value, expiration) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, interface{}, time.Duration) *redis.StatusCmd); ok { + r0 = returnFunc(ctx, key, value, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// redisClientMock_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type redisClientMock_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value interface{} +// - expiration time.Duration +func (_e *redisClientMock_Expecter) Set(ctx interface{}, key interface{}, value interface{}, expiration interface{}) *redisClientMock_Set_Call { + return &redisClientMock_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expiration)} +} + +func (_c *redisClientMock_Set_Call) Run(run func(ctx context.Context, key string, value interface{}, expiration time.Duration)) *redisClientMock_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 interface{} + if args[2] != nil { + arg2 = args[2].(interface{}) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *redisClientMock_Set_Call) Return(statusCmd *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *redisClientMock_Set_Call) RunAndReturn(run func(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/internal/authn/passkey/redis_store.go b/backend/internal/authn/passkey/redis_store.go new file mode 100644 index 000000000..22e13e80f --- /dev/null +++ b/backend/internal/authn/passkey/redis_store.go @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package passkey + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/internal/system/database/provider" +) + +// redisClient abstracts the Redis commands used by the passkey session store. +type redisClient interface { + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd + Get(ctx context.Context, key string) *redis.StringCmd + Del(ctx context.Context, keys ...string) *redis.IntCmd +} + +// redisSessionStore is the Redis-backed implementation of sessionStoreInterface. +type redisSessionStore struct { + client redisClient + keyPrefix string + deploymentID string +} + +// newRedisSessionStore creates a new Redis-backed passkey session store. +func newRedisSessionStore(p provider.RedisProviderInterface) sessionStoreInterface { + return &redisSessionStore{ + client: p.GetRedisClient(), + keyPrefix: p.GetKeyPrefix(), + deploymentID: config.GetThunderRuntime().Config.Server.Identifier, + } +} + +// sessionKey builds the Redis key for a passkey session. +func (s *redisSessionStore) sessionKey(key string) string { + return fmt.Sprintf("%s:runtime:%s:passkey:%s", s.keyPrefix, s.deploymentID, key) +} + +// storeSession serializes the WebAuthn session data and stores it in Redis with a TTL. +func (s *redisSessionStore) storeSession(sessionKey string, session *sessionData, expirySeconds int64) error { + data, err := json.Marshal(session) + if err != nil { + return fmt.Errorf("failed to marshal passkey session: %w", err) + } + + ttl := time.Duration(expirySeconds) * time.Second + if err := s.client.Set(context.Background(), s.sessionKey(sessionKey), data, ttl).Err(); err != nil { + return fmt.Errorf("failed to store passkey session in Redis: %w", err) + } + + return nil +} + +// retrieveSession retrieves the WebAuthn session data from Redis. +func (s *redisSessionStore) retrieveSession(sessionKey string) (*sessionData, error) { + if sessionKey == "" { + return nil, nil + } + + data, err := s.client.Get(context.Background(), s.sessionKey(sessionKey)).Bytes() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, nil + } + return nil, fmt.Errorf("failed to get passkey session from Redis: %w", err) + } + + var result sessionData + if err := json.Unmarshal(data, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal passkey session: %w", err) + } + + return &result, nil +} + +// deleteSession removes the passkey session from Redis. +func (s *redisSessionStore) deleteSession(sessionKey string) error { + if sessionKey == "" { + return nil + } + + if err := s.client.Del(context.Background(), s.sessionKey(sessionKey)).Err(); err != nil { + return fmt.Errorf("failed to delete passkey session from Redis: %w", err) + } + + return nil +} diff --git a/backend/internal/authn/passkey/redis_store_test.go b/backend/internal/authn/passkey/redis_store_test.go new file mode 100644 index 000000000..0432d1fd9 --- /dev/null +++ b/backend/internal/authn/passkey/redis_store_test.go @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package passkey + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/suite" +) + +const ( + redisTestKeyPrefix = "thunder" + redisTestDeploymentID = "test-deployment" +) + +type RedisSessionStoreTestSuite struct { + suite.Suite + store *redisSessionStore + mockClient *redisClientMock + sessionKey string + redisKey string +} + +func TestRedisSessionStoreSuite(t *testing.T) { + suite.Run(t, new(RedisSessionStoreTestSuite)) +} + +func (suite *RedisSessionStoreTestSuite) SetupTest() { + suite.mockClient = newRedisClientMock(suite.T()) + suite.store = &redisSessionStore{ + client: suite.mockClient, + keyPrefix: redisTestKeyPrefix, + deploymentID: redisTestDeploymentID, + } + suite.sessionKey = testSessionKey + suite.redisKey = fmt.Sprintf("%s:runtime:%s:passkey:%s", + redisTestKeyPrefix, redisTestDeploymentID, testSessionKey) +} + +// Tests for sessionKey + +func (suite *RedisSessionStoreTestSuite) TestSessionKey() { + key := suite.store.sessionKey(testSessionKey) + suite.Equal(suite.redisKey, key) +} + +// Tests for storeSession + +func (suite *RedisSessionStoreTestSuite) TestStoreSession_Success() { + sd := &sessionData{ + Challenge: "challenge123", + UserID: []byte(testUserID), + RelyingPartyID: testRelyingPartyID, + UserVerification: "preferred", + } + + statusCmd := redis.NewStatusCmd(context.Background()) + suite.mockClient.On("Set", context.Background(), suite.redisKey, + suite.serializedSessionData(sd), 300*time.Second).Return(statusCmd) + + err := suite.store.storeSession(testSessionKey, sd, 300) + suite.NoError(err) +} + +func (suite *RedisSessionStoreTestSuite) TestStoreSession_SetError() { + sd := &sessionData{Challenge: "challenge123"} + + statusCmd := redis.NewStatusCmd(context.Background()) + statusCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Set", context.Background(), suite.redisKey, + suite.serializedSessionData(sd), 300*time.Second).Return(statusCmd) + + err := suite.store.storeSession(testSessionKey, sd, 300) + suite.Error(err) + suite.Contains(err.Error(), "failed to store passkey session in Redis") +} + +// Tests for retrieveSession + +func (suite *RedisSessionStoreTestSuite) TestRetrieveSession_Success() { + sd := &sessionData{ + Challenge: "challenge123", + UserID: []byte(testUserID), + RelyingPartyID: testRelyingPartyID, + UserVerification: "preferred", + } + + data, _ := json.Marshal(sd) + stringCmd := redis.NewStringCmd(context.Background()) + stringCmd.SetVal(string(data)) + suite.mockClient.On("Get", context.Background(), suite.redisKey).Return(stringCmd) + + result, err := suite.store.retrieveSession(testSessionKey) + suite.NoError(err) + suite.NotNil(result) + suite.Equal("challenge123", result.Challenge) + suite.Equal(testRelyingPartyID, result.RelyingPartyID) +} + +func (suite *RedisSessionStoreTestSuite) TestRetrieveSession_EmptyKey() { + result, err := suite.store.retrieveSession("") + suite.NoError(err) + suite.Nil(result) +} + +func (suite *RedisSessionStoreTestSuite) TestRetrieveSession_NotFound() { + stringCmd := redis.NewStringCmd(context.Background()) + stringCmd.SetErr(redis.Nil) + suite.mockClient.On("Get", context.Background(), suite.redisKey).Return(stringCmd) + + result, err := suite.store.retrieveSession(testSessionKey) + suite.NoError(err) + suite.Nil(result) +} + +func (suite *RedisSessionStoreTestSuite) TestRetrieveSession_GetError() { + stringCmd := redis.NewStringCmd(context.Background()) + stringCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Get", context.Background(), suite.redisKey).Return(stringCmd) + + result, err := suite.store.retrieveSession(testSessionKey) + suite.Error(err) + suite.Contains(err.Error(), "failed to get passkey session from Redis") + suite.Nil(result) +} + +func (suite *RedisSessionStoreTestSuite) TestRetrieveSession_UnmarshalError() { + stringCmd := redis.NewStringCmd(context.Background()) + stringCmd.SetVal("not valid json{{{") + suite.mockClient.On("Get", context.Background(), suite.redisKey).Return(stringCmd) + + result, err := suite.store.retrieveSession(testSessionKey) + suite.Error(err) + suite.Contains(err.Error(), "failed to unmarshal passkey session") + suite.Nil(result) +} + +// Tests for deleteSession + +func (suite *RedisSessionStoreTestSuite) TestDeleteSession_Success() { + intCmd := redis.NewIntCmd(context.Background()) + intCmd.SetVal(1) + suite.mockClient.On("Del", context.Background(), suite.redisKey).Return(intCmd) + + err := suite.store.deleteSession(testSessionKey) + suite.NoError(err) +} + +func (suite *RedisSessionStoreTestSuite) TestDeleteSession_EmptyKey() { + err := suite.store.deleteSession("") + suite.NoError(err) +} + +func (suite *RedisSessionStoreTestSuite) TestDeleteSession_DelError() { + intCmd := redis.NewIntCmd(context.Background()) + intCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Del", context.Background(), suite.redisKey).Return(intCmd) + + err := suite.store.deleteSession(testSessionKey) + suite.Error(err) + suite.Contains(err.Error(), "failed to delete passkey session from Redis") +} + +// serializedSessionData marshals sessionData to []byte for use in mock matchers. +func (suite *RedisSessionStoreTestSuite) serializedSessionData(sd *sessionData) []byte { + data, err := json.Marshal(sd) + suite.Require().NoError(err) + return data +} diff --git a/backend/internal/entityprovider/init_test.go b/backend/internal/entityprovider/init_test.go index 2ed322cc7..0976e862a 100644 --- a/backend/internal/entityprovider/init_test.go +++ b/backend/internal/entityprovider/init_test.go @@ -38,12 +38,12 @@ func (suite *InitEntityProviderTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } diff --git a/backend/internal/flow/flowexec/init.go b/backend/internal/flow/flowexec/init.go index 2268c1655..feba58ae8 100644 --- a/backend/internal/flow/flowexec/init.go +++ b/backend/internal/flow/flowexec/init.go @@ -24,9 +24,11 @@ import ( "github.com/asgardeo/thunder/internal/application" "github.com/asgardeo/thunder/internal/flow/executor" flowmgt "github.com/asgardeo/thunder/internal/flow/mgt" + "github.com/asgardeo/thunder/internal/system/config" dbprovider "github.com/asgardeo/thunder/internal/system/database/provider" "github.com/asgardeo/thunder/internal/system/middleware" "github.com/asgardeo/thunder/internal/system/observability" + "github.com/asgardeo/thunder/internal/system/transaction" ) // Initialize creates and configures the flow execution service components. @@ -38,12 +40,21 @@ func Initialize( executorRegistry executor.ExecutorRegistryInterface, observabilitySvc observability.ObservabilityServiceInterface, ) (FlowExecServiceInterface, error) { - dbProvider := dbprovider.GetDBProvider() - transactioner, err := dbProvider.GetRuntimeDBTransactioner() - if err != nil { - return nil, err + var flowStore flowStoreInterface + var transactioner transaction.Transactioner + + if config.GetThunderRuntime().Config.Database.Runtime.Type == dbprovider.DataSourceTypeRedis { + flowStore = newRedisFlowStore(dbprovider.GetRedisProvider()) + transactioner = transaction.NewNoOpTransactioner() + } else { + dbProvider := dbprovider.GetDBProvider() + var err error + transactioner, err = dbProvider.GetRuntimeDBTransactioner() + if err != nil { + return nil, err + } + flowStore = newFlowStore(dbProvider) } - flowStore := newFlowStore(dbProvider) flowEngine := newFlowEngine(executorRegistry, observabilitySvc) flowExecService := newFlowExecService(flowMgtService, flowStore, flowEngine, applicationService, observabilitySvc, transactioner) diff --git a/backend/internal/flow/flowexec/redisClient_mock_test.go b/backend/internal/flow/flowexec/redisClient_mock_test.go new file mode 100644 index 000000000..1ff8cfc76 --- /dev/null +++ b/backend/internal/flow/flowexec/redisClient_mock_test.go @@ -0,0 +1,689 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package flowexec + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// newRedisClientMock creates a new instance of redisClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newRedisClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *redisClientMock { + mock := &redisClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// redisClientMock is an autogenerated mock type for the redisClient type +type redisClientMock struct { + mock.Mock +} + +type redisClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *redisClientMock) EXPECT() *redisClientMock_Expecter { + return &redisClientMock_Expecter{mock: &_m.Mock} +} + +// Del provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Del(ctx context.Context, keys ...string) *redis.IntCmd { + // string + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Del") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// redisClientMock_Del_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Del' +type redisClientMock_Del_Call struct { + *mock.Call +} + +// Del is a helper method to define mock.On call +// - ctx context.Context +// - keys ...string +func (_e *redisClientMock_Expecter) Del(ctx interface{}, keys ...interface{}) *redisClientMock_Del_Call { + return &redisClientMock_Del_Call{Call: _e.mock.On("Del", + append([]interface{}{ctx}, keys...)...)} +} + +func (_c *redisClientMock_Del_Call) Run(run func(ctx context.Context, keys ...string)) *redisClientMock_Del_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *redisClientMock_Del_Call) Return(intCmd *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *redisClientMock_Del_Call) RunAndReturn(run func(ctx context.Context, keys ...string) *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(run) + return _c +} + +// Eval provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, script, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Eval") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, script, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// redisClientMock_Eval_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Eval' +type redisClientMock_Eval_Call struct { + *mock.Call +} + +// Eval is a helper method to define mock.On call +// - ctx context.Context +// - script string +// - keys []string +// - args ...interface{} +func (_e *redisClientMock_Expecter) Eval(ctx interface{}, script interface{}, keys interface{}, args ...interface{}) *redisClientMock_Eval_Call { + return &redisClientMock_Eval_Call{Call: _e.mock.On("Eval", + append([]interface{}{ctx, script, keys}, args...)...)} +} + +func (_c *redisClientMock_Eval_Call) Run(run func(ctx context.Context, script string, keys []string, args ...interface{})) *redisClientMock_Eval_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *redisClientMock_Eval_Call) Return(cmd *redis.Cmd) *redisClientMock_Eval_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *redisClientMock_Eval_Call) RunAndReturn(run func(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd) *redisClientMock_Eval_Call { + _c.Call.Return(run) + return _c +} + +// EvalRO provides a mock function for the type redisClientMock +func (_mock *redisClientMock) EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, script, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalRO") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, script, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// redisClientMock_EvalRO_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalRO' +type redisClientMock_EvalRO_Call struct { + *mock.Call +} + +// EvalRO is a helper method to define mock.On call +// - ctx context.Context +// - script string +// - keys []string +// - args ...interface{} +func (_e *redisClientMock_Expecter) EvalRO(ctx interface{}, script interface{}, keys interface{}, args ...interface{}) *redisClientMock_EvalRO_Call { + return &redisClientMock_EvalRO_Call{Call: _e.mock.On("EvalRO", + append([]interface{}{ctx, script, keys}, args...)...)} +} + +func (_c *redisClientMock_EvalRO_Call) Run(run func(ctx context.Context, script string, keys []string, args ...interface{})) *redisClientMock_EvalRO_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *redisClientMock_EvalRO_Call) Return(cmd *redis.Cmd) *redisClientMock_EvalRO_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *redisClientMock_EvalRO_Call) RunAndReturn(run func(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd) *redisClientMock_EvalRO_Call { + _c.Call.Return(run) + return _c +} + +// EvalSha provides a mock function for the type redisClientMock +func (_mock *redisClientMock) EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, sha1, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalSha") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, sha1, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// redisClientMock_EvalSha_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalSha' +type redisClientMock_EvalSha_Call struct { + *mock.Call +} + +// EvalSha is a helper method to define mock.On call +// - ctx context.Context +// - sha1 string +// - keys []string +// - args ...interface{} +func (_e *redisClientMock_Expecter) EvalSha(ctx interface{}, sha1 interface{}, keys interface{}, args ...interface{}) *redisClientMock_EvalSha_Call { + return &redisClientMock_EvalSha_Call{Call: _e.mock.On("EvalSha", + append([]interface{}{ctx, sha1, keys}, args...)...)} +} + +func (_c *redisClientMock_EvalSha_Call) Run(run func(ctx context.Context, sha1 string, keys []string, args ...interface{})) *redisClientMock_EvalSha_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *redisClientMock_EvalSha_Call) Return(cmd *redis.Cmd) *redisClientMock_EvalSha_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *redisClientMock_EvalSha_Call) RunAndReturn(run func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd) *redisClientMock_EvalSha_Call { + _c.Call.Return(run) + return _c +} + +// EvalShaRO provides a mock function for the type redisClientMock +func (_mock *redisClientMock) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, sha1, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalShaRO") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, sha1, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// redisClientMock_EvalShaRO_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalShaRO' +type redisClientMock_EvalShaRO_Call struct { + *mock.Call +} + +// EvalShaRO is a helper method to define mock.On call +// - ctx context.Context +// - sha1 string +// - keys []string +// - args ...interface{} +func (_e *redisClientMock_Expecter) EvalShaRO(ctx interface{}, sha1 interface{}, keys interface{}, args ...interface{}) *redisClientMock_EvalShaRO_Call { + return &redisClientMock_EvalShaRO_Call{Call: _e.mock.On("EvalShaRO", + append([]interface{}{ctx, sha1, keys}, args...)...)} +} + +func (_c *redisClientMock_EvalShaRO_Call) Run(run func(ctx context.Context, sha1 string, keys []string, args ...interface{})) *redisClientMock_EvalShaRO_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *redisClientMock_EvalShaRO_Call) Return(cmd *redis.Cmd) *redisClientMock_EvalShaRO_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *redisClientMock_EvalShaRO_Call) RunAndReturn(run func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd) *redisClientMock_EvalShaRO_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Get(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// redisClientMock_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type redisClientMock_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *redisClientMock_Expecter) Get(ctx interface{}, key interface{}) *redisClientMock_Get_Call { + return &redisClientMock_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *redisClientMock_Get_Call) Run(run func(ctx context.Context, key string)) *redisClientMock_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_Get_Call) Return(stringCmd *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *redisClientMock_Get_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(run) + return _c +} + +// ScriptExists provides a mock function for the type redisClientMock +func (_mock *redisClientMock) ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd { + // string + _va := make([]interface{}, len(hashes)) + for _i := range hashes { + _va[_i] = hashes[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for ScriptExists") + } + + var r0 *redis.BoolSliceCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.BoolSliceCmd); ok { + r0 = returnFunc(ctx, hashes...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.BoolSliceCmd) + } + } + return r0 +} + +// redisClientMock_ScriptExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScriptExists' +type redisClientMock_ScriptExists_Call struct { + *mock.Call +} + +// ScriptExists is a helper method to define mock.On call +// - ctx context.Context +// - hashes ...string +func (_e *redisClientMock_Expecter) ScriptExists(ctx interface{}, hashes ...interface{}) *redisClientMock_ScriptExists_Call { + return &redisClientMock_ScriptExists_Call{Call: _e.mock.On("ScriptExists", + append([]interface{}{ctx}, hashes...)...)} +} + +func (_c *redisClientMock_ScriptExists_Call) Run(run func(ctx context.Context, hashes ...string)) *redisClientMock_ScriptExists_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *redisClientMock_ScriptExists_Call) Return(boolSliceCmd *redis.BoolSliceCmd) *redisClientMock_ScriptExists_Call { + _c.Call.Return(boolSliceCmd) + return _c +} + +func (_c *redisClientMock_ScriptExists_Call) RunAndReturn(run func(ctx context.Context, hashes ...string) *redis.BoolSliceCmd) *redisClientMock_ScriptExists_Call { + _c.Call.Return(run) + return _c +} + +// ScriptLoad provides a mock function for the type redisClientMock +func (_mock *redisClientMock) ScriptLoad(ctx context.Context, script string) *redis.StringCmd { + ret := _mock.Called(ctx, script) + + if len(ret) == 0 { + panic("no return value specified for ScriptLoad") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, script) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// redisClientMock_ScriptLoad_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScriptLoad' +type redisClientMock_ScriptLoad_Call struct { + *mock.Call +} + +// ScriptLoad is a helper method to define mock.On call +// - ctx context.Context +// - script string +func (_e *redisClientMock_Expecter) ScriptLoad(ctx interface{}, script interface{}) *redisClientMock_ScriptLoad_Call { + return &redisClientMock_ScriptLoad_Call{Call: _e.mock.On("ScriptLoad", ctx, script)} +} + +func (_c *redisClientMock_ScriptLoad_Call) Run(run func(ctx context.Context, script string)) *redisClientMock_ScriptLoad_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_ScriptLoad_Call) Return(stringCmd *redis.StringCmd) *redisClientMock_ScriptLoad_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *redisClientMock_ScriptLoad_Call) RunAndReturn(run func(ctx context.Context, script string) *redis.StringCmd) *redisClientMock_ScriptLoad_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + ret := _mock.Called(ctx, key, value, expiration) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, interface{}, time.Duration) *redis.StatusCmd); ok { + r0 = returnFunc(ctx, key, value, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// redisClientMock_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type redisClientMock_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value interface{} +// - expiration time.Duration +func (_e *redisClientMock_Expecter) Set(ctx interface{}, key interface{}, value interface{}, expiration interface{}) *redisClientMock_Set_Call { + return &redisClientMock_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expiration)} +} + +func (_c *redisClientMock_Set_Call) Run(run func(ctx context.Context, key string, value interface{}, expiration time.Duration)) *redisClientMock_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 interface{} + if args[2] != nil { + arg2 = args[2].(interface{}) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *redisClientMock_Set_Call) Return(statusCmd *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *redisClientMock_Set_Call) RunAndReturn(run func(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/internal/flow/flowexec/redis_store.go b/backend/internal/flow/flowexec/redis_store.go new file mode 100644 index 000000000..a94abd1b9 --- /dev/null +++ b/backend/internal/flow/flowexec/redis_store.go @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package flowexec + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/internal/system/database/provider" + "github.com/asgardeo/thunder/internal/system/log" +) + +// updateFlowScript atomically updates a flow context preserving its TTL. +// Returns 1 on success, 0 if the key does not exist. +var updateFlowScript = redis.NewScript(` +if redis.call('EXISTS', KEYS[1]) == 0 then return 0 end +redis.call('SET', KEYS[1], ARGV[1], 'KEEPTTL') +return 1 +`) + +// redisClient abstracts the Redis commands used by the flow store. +type redisClient interface { + redis.Scripter + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd + Get(ctx context.Context, key string) *redis.StringCmd + Del(ctx context.Context, keys ...string) *redis.IntCmd +} + +// redisFlowStore is the Redis-backed implementation of flowStoreInterface. +type redisFlowStore struct { + client redisClient + keyPrefix string + deploymentID string +} + +// newRedisFlowStore creates a new Redis-backed flow store. +func newRedisFlowStore(p provider.RedisProviderInterface) flowStoreInterface { + return &redisFlowStore{ + client: p.GetRedisClient(), + keyPrefix: p.GetKeyPrefix(), + deploymentID: config.GetThunderRuntime().Config.Server.Identifier, + } +} + +// flowKey builds the Redis key for a flow context. +func (s *redisFlowStore) flowKey(flowID string) string { + return fmt.Sprintf("%s:runtime:%s:flow:%s", s.keyPrefix, s.deploymentID, flowID) +} + +// StoreFlowContext serializes the engine context and stores it in Redis with a TTL. +func (s *redisFlowStore) StoreFlowContext(ctx context.Context, engineCtx EngineContext, expirySeconds int64) error { + logger := log.GetLogger().With(log.String(log.LoggerKeyComponentName, "RedisFlowStore")) + + dbModel, err := FromEngineContext(engineCtx) + if err != nil { + return fmt.Errorf("failed to convert engine context to db model: %w", err) + } + + data, err := json.Marshal(dbModel) + if err != nil { + return fmt.Errorf("failed to marshal flow context: %w", err) + } + + ttl := time.Duration(expirySeconds) * time.Second + if err := s.client.Set(ctx, s.flowKey(engineCtx.FlowID), data, ttl).Err(); err != nil { + return fmt.Errorf("failed to store flow context in Redis: %w", err) + } + + logger.Debug("Stored flow context in Redis", log.String("flowID", engineCtx.FlowID)) + return nil +} + +// GetFlowContext retrieves the flow context from Redis. +func (s *redisFlowStore) GetFlowContext(ctx context.Context, flowID string) (*FlowContextDB, error) { + data, err := s.client.Get(ctx, s.flowKey(flowID)).Bytes() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, nil + } + return nil, fmt.Errorf("failed to get flow context from Redis: %w", err) + } + + var result FlowContextDB + if err := json.Unmarshal(data, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal flow context: %w", err) + } + + return &result, nil +} + +// UpdateFlowContext updates the stored flow context, preserving the remaining TTL. +func (s *redisFlowStore) UpdateFlowContext(ctx context.Context, engineCtx EngineContext) error { + key := s.flowKey(engineCtx.FlowID) + + dbModel, err := FromEngineContext(engineCtx) + if err != nil { + return fmt.Errorf("failed to convert engine context to db model: %w", err) + } + + data, err := json.Marshal(dbModel) + if err != nil { + return fmt.Errorf("failed to marshal flow context: %w", err) + } + + n, err := updateFlowScript.Run(ctx, s.client, []string{key}, data).Int() + if err != nil { + return fmt.Errorf("failed to update flow context in Redis: %w", err) + } + if n == 0 { + return fmt.Errorf("flow context not found for flowID: %s", engineCtx.FlowID) + } + + return nil +} + +// DeleteFlowContext removes the flow context from Redis. +func (s *redisFlowStore) DeleteFlowContext(ctx context.Context, flowID string) error { + if err := s.client.Del(ctx, s.flowKey(flowID)).Err(); err != nil { + return fmt.Errorf("failed to delete flow context from Redis: %w", err) + } + return nil +} diff --git a/backend/internal/flow/flowexec/redis_store_test.go b/backend/internal/flow/flowexec/redis_store_test.go new file mode 100644 index 000000000..6b1110a61 --- /dev/null +++ b/backend/internal/flow/flowexec/redis_store_test.go @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package flowexec + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + authncm "github.com/asgardeo/thunder/internal/authn/common" + "github.com/asgardeo/thunder/internal/flow/common" + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/tests/mocks/flow/coremock" +) + +const ( + redisTestKeyPrefix = "thunder" + redisTestDeploymentID = "test-redis-deployment" + redisTestFlowID = "test-flow-id" +) + +type RedisFlowStoreTestSuite struct { + suite.Suite + store *redisFlowStore + mockClient *redisClientMock + ctx context.Context + flowKey string +} + +func TestRedisFlowStoreSuite(t *testing.T) { + testConfig := &config.Config{ + Crypto: config.CryptoConfig{ + Encryption: config.EncryptionConfig{ + Key: "2729a7928c79371e5f312167269294a14bb0660fd166b02a408a20fa73271580", + }, + }, + Server: config.ServerConfig{ + Identifier: redisTestDeploymentID, + }, + } + config.ResetThunderRuntime() + if err := config.InitializeThunderRuntime("/test/thunder/home", testConfig); err != nil { + t.Fatalf("Failed to initialize Thunder runtime: %v", err) + } + t.Cleanup(config.ResetThunderRuntime) + + suite.Run(t, new(RedisFlowStoreTestSuite)) +} + +func (suite *RedisFlowStoreTestSuite) SetupTest() { + suite.mockClient = newRedisClientMock(suite.T()) + suite.ctx = context.Background() + suite.store = &redisFlowStore{ + client: suite.mockClient, + keyPrefix: redisTestKeyPrefix, + deploymentID: redisTestDeploymentID, + } + suite.flowKey = fmt.Sprintf("%s:runtime:%s:flow:%s", + redisTestKeyPrefix, redisTestDeploymentID, redisTestFlowID) +} + +// buildEngineContext creates a minimal EngineContext for use in tests. +// GetID is registered as Maybe because not all test paths reach FromEngineContext. +func (suite *RedisFlowStoreTestSuite) buildEngineContext() EngineContext { + mockGraph := coremock.NewGraphInterfaceMock(suite.T()) + mockGraph.On("GetID").Return("test-graph-id").Maybe() + return EngineContext{ + FlowID: redisTestFlowID, + AppID: "test-app-id", + FlowType: common.FlowTypeAuthentication, + AuthenticatedUser: authncm.AuthenticatedUser{ + IsAuthenticated: false, + Attributes: map[string]interface{}{}, + }, + UserInputs: map[string]string{}, + RuntimeData: map[string]string{}, + ExecutionHistory: map[string]*common.NodeExecutionRecord{}, + Graph: mockGraph, + } +} + +// serializedFlowContext converts an EngineContext to the JSON bytes the store would write. +func (suite *RedisFlowStoreTestSuite) serializedFlowContext(engineCtx EngineContext) []byte { + dbModel, err := FromEngineContext(engineCtx) + suite.Require().NoError(err) + data, err := json.Marshal(dbModel) + suite.Require().NoError(err) + return data +} + +// Tests for flowKey + +func (suite *RedisFlowStoreTestSuite) TestFlowKey() { + key := suite.store.flowKey(redisTestFlowID) + suite.Equal(suite.flowKey, key) +} + +// Tests for StoreFlowContext + +func (suite *RedisFlowStoreTestSuite) TestStoreFlowContext_Success() { + engineCtx := suite.buildEngineContext() + expirySeconds := int64(1800) + + statusCmd := redis.NewStatusCmd(suite.ctx) + suite.mockClient.On("Set", suite.ctx, suite.flowKey, mock.Anything, + time.Duration(expirySeconds)*time.Second).Return(statusCmd) + + err := suite.store.StoreFlowContext(suite.ctx, engineCtx, expirySeconds) + suite.NoError(err) +} + +func (suite *RedisFlowStoreTestSuite) TestStoreFlowContext_SetError() { + engineCtx := suite.buildEngineContext() + expirySeconds := int64(1800) + + statusCmd := redis.NewStatusCmd(suite.ctx) + statusCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Set", suite.ctx, suite.flowKey, mock.Anything, + time.Duration(expirySeconds)*time.Second).Return(statusCmd) + + err := suite.store.StoreFlowContext(suite.ctx, engineCtx, expirySeconds) + suite.Error(err) + suite.Contains(err.Error(), "failed to store flow context in Redis") +} + +// Tests for GetFlowContext + +func (suite *RedisFlowStoreTestSuite) TestGetFlowContext_Success() { + engineCtx := suite.buildEngineContext() + data := suite.serializedFlowContext(engineCtx) + + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal(string(data)) + suite.mockClient.On("Get", suite.ctx, suite.flowKey).Return(stringCmd) + + result, err := suite.store.GetFlowContext(suite.ctx, redisTestFlowID) + suite.NoError(err) + suite.NotNil(result) + suite.Equal(redisTestFlowID, result.FlowID) +} + +func (suite *RedisFlowStoreTestSuite) TestGetFlowContext_NotFound() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetErr(redis.Nil) + suite.mockClient.On("Get", suite.ctx, suite.flowKey).Return(stringCmd) + + result, err := suite.store.GetFlowContext(suite.ctx, redisTestFlowID) + suite.NoError(err) + suite.Nil(result) +} + +func (suite *RedisFlowStoreTestSuite) TestGetFlowContext_GetError() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Get", suite.ctx, suite.flowKey).Return(stringCmd) + + result, err := suite.store.GetFlowContext(suite.ctx, redisTestFlowID) + suite.Error(err) + suite.Contains(err.Error(), "failed to get flow context from Redis") + suite.Nil(result) +} + +func (suite *RedisFlowStoreTestSuite) TestGetFlowContext_UnmarshalError() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal("not valid json{{{") + suite.mockClient.On("Get", suite.ctx, suite.flowKey).Return(stringCmd) + + result, err := suite.store.GetFlowContext(suite.ctx, redisTestFlowID) + suite.Error(err) + suite.Contains(err.Error(), "failed to unmarshal flow context") + suite.Nil(result) +} + +// Tests for UpdateFlowContext + +func (suite *RedisFlowStoreTestSuite) TestUpdateFlowContext_Success() { + engineCtx := suite.buildEngineContext() + + cmd := redis.NewCmd(suite.ctx) + cmd.SetVal(int64(1)) + suite.mockClient.On("EvalSha", suite.ctx, updateFlowScript.Hash(), + []string{suite.flowKey}, mock.Anything).Return(cmd) + + err := suite.store.UpdateFlowContext(suite.ctx, engineCtx) + suite.NoError(err) +} + +func (suite *RedisFlowStoreTestSuite) TestUpdateFlowContext_KeyNotFound() { + engineCtx := suite.buildEngineContext() + + cmd := redis.NewCmd(suite.ctx) + cmd.SetVal(int64(0)) + suite.mockClient.On("EvalSha", suite.ctx, updateFlowScript.Hash(), + []string{suite.flowKey}, mock.Anything).Return(cmd) + + err := suite.store.UpdateFlowContext(suite.ctx, engineCtx) + suite.Error(err) + suite.Contains(err.Error(), "flow context not found for flowID") +} + +func (suite *RedisFlowStoreTestSuite) TestUpdateFlowContext_ScriptError() { + engineCtx := suite.buildEngineContext() + + cmd := redis.NewCmd(suite.ctx) + cmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("EvalSha", suite.ctx, updateFlowScript.Hash(), + []string{suite.flowKey}, mock.Anything).Return(cmd) + + err := suite.store.UpdateFlowContext(suite.ctx, engineCtx) + suite.Error(err) + suite.Contains(err.Error(), "failed to update flow context in Redis") +} + +// Tests for DeleteFlowContext + +func (suite *RedisFlowStoreTestSuite) TestDeleteFlowContext_Success() { + intCmd := redis.NewIntCmd(suite.ctx) + intCmd.SetVal(1) + suite.mockClient.On("Del", suite.ctx, suite.flowKey).Return(intCmd) + + err := suite.store.DeleteFlowContext(suite.ctx, redisTestFlowID) + suite.NoError(err) +} + +func (suite *RedisFlowStoreTestSuite) TestDeleteFlowContext_DelError() { + intCmd := redis.NewIntCmd(suite.ctx) + intCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Del", suite.ctx, suite.flowKey).Return(intCmd) + + err := suite.store.DeleteFlowContext(suite.ctx, redisTestFlowID) + suite.Error(err) + suite.Contains(err.Error(), "failed to delete flow context from Redis") +} diff --git a/backend/internal/flow/mgt/init_test.go b/backend/internal/flow/mgt/init_test.go index 1eec5abc1..a99ca3283 100644 --- a/backend/internal/flow/mgt/init_test.go +++ b/backend/internal/flow/mgt/init_test.go @@ -63,12 +63,12 @@ func (s *InitTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -726,12 +726,12 @@ func (s *InitTestSuite) TestInitializeStore_MutableMode() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -763,12 +763,12 @@ func (s *InitTestSuite) TestInitializeStore_DeclarativeMode() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -802,12 +802,12 @@ func (s *InitTestSuite) TestInitializeStore_CompositeMode() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -846,12 +846,12 @@ func (s *InitTestSuite) TestInitializeStore_DeclarativeMode_ResourceLoadingError }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -887,12 +887,12 @@ func (s *InitTestSuite) TestInitializeStore_CompositeMode_ResourceLoadingError() }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -931,12 +931,12 @@ func (s *InitTestSuite) TestInitializeStore_DefaultMode() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -971,12 +971,12 @@ func (s *InitTestSuite) TestInitializeStore_ModeNormalization() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ diff --git a/backend/internal/idp/init_test.go b/backend/internal/idp/init_test.go index 3f3c261c1..6c0614328 100644 --- a/backend/internal/idp/init_test.go +++ b/backend/internal/idp/init_test.go @@ -69,16 +69,16 @@ func (s *IDPInitTestSuite) TestInitialize() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, User: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -297,16 +297,16 @@ func (suite *IDPInitTestSuite) TestInitialize_WithDeclarativeResourcesDisabled() }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, User: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -334,16 +334,16 @@ func TestInitialize_WithDeclarativeResourcesEnabled_EmptyDirectory(t *testing.T) }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, User: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, }, } @@ -396,16 +396,16 @@ func TestInitialize_WithDeclarativeResourcesEnabled_ValidConfigs(t *testing.T) { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, User: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, }, DeclarativeResources: config.DeclarativeResources{ @@ -534,16 +534,16 @@ func TestInitialize_WithDeclarativeResourcesEnabled_InvalidYAML(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, User: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, }, } @@ -595,16 +595,16 @@ properties: }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, User: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, }, } @@ -656,16 +656,16 @@ properties: }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, User: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, }, } diff --git a/backend/internal/idp/store_test.go b/backend/internal/idp/store_test.go index bffd56ace..9dd7cab40 100644 --- a/backend/internal/idp/store_test.go +++ b/backend/internal/idp/store_test.go @@ -48,12 +48,12 @@ func (s *IDPStoreTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } diff --git a/backend/internal/notification/init_test.go b/backend/internal/notification/init_test.go index e056564c6..0f81358ca 100644 --- a/backend/internal/notification/init_test.go +++ b/backend/internal/notification/init_test.go @@ -62,8 +62,8 @@ func (suite *InitTestSuite) SetupSuite() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -208,8 +208,8 @@ properties: }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -501,8 +501,8 @@ func (suite *InitTestSuite) TestInitialize_WithDeclarativeResourcesEnabled_Inval }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, DeclarativeResources: config.DeclarativeResources{ @@ -533,8 +533,8 @@ func (suite *InitTestSuite) TestInitialize_WithDeclarativeResourcesEnabled_Inval }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -579,8 +579,8 @@ properties: }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, DeclarativeResources: config.DeclarativeResources{ @@ -611,8 +611,8 @@ properties: }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } diff --git a/backend/internal/oauth/oauth2/authz/authCodeRedisClient_mock_test.go b/backend/internal/oauth/oauth2/authz/authCodeRedisClient_mock_test.go new file mode 100644 index 000000000..a8356cd4e --- /dev/null +++ b/backend/internal/oauth/oauth2/authz/authCodeRedisClient_mock_test.go @@ -0,0 +1,617 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package authz + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// newAuthCodeRedisClientMock creates a new instance of authCodeRedisClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newAuthCodeRedisClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *authCodeRedisClientMock { + mock := &authCodeRedisClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// authCodeRedisClientMock is an autogenerated mock type for the authCodeRedisClient type +type authCodeRedisClientMock struct { + mock.Mock +} + +type authCodeRedisClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *authCodeRedisClientMock) EXPECT() *authCodeRedisClientMock_Expecter { + return &authCodeRedisClientMock_Expecter{mock: &_m.Mock} +} + +// Eval provides a mock function for the type authCodeRedisClientMock +func (_mock *authCodeRedisClientMock) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, script, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Eval") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, script, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// authCodeRedisClientMock_Eval_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Eval' +type authCodeRedisClientMock_Eval_Call struct { + *mock.Call +} + +// Eval is a helper method to define mock.On call +// - ctx context.Context +// - script string +// - keys []string +// - args ...interface{} +func (_e *authCodeRedisClientMock_Expecter) Eval(ctx interface{}, script interface{}, keys interface{}, args ...interface{}) *authCodeRedisClientMock_Eval_Call { + return &authCodeRedisClientMock_Eval_Call{Call: _e.mock.On("Eval", + append([]interface{}{ctx, script, keys}, args...)...)} +} + +func (_c *authCodeRedisClientMock_Eval_Call) Run(run func(ctx context.Context, script string, keys []string, args ...interface{})) *authCodeRedisClientMock_Eval_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *authCodeRedisClientMock_Eval_Call) Return(cmd *redis.Cmd) *authCodeRedisClientMock_Eval_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *authCodeRedisClientMock_Eval_Call) RunAndReturn(run func(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd) *authCodeRedisClientMock_Eval_Call { + _c.Call.Return(run) + return _c +} + +// EvalRO provides a mock function for the type authCodeRedisClientMock +func (_mock *authCodeRedisClientMock) EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, script, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalRO") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, script, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// authCodeRedisClientMock_EvalRO_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalRO' +type authCodeRedisClientMock_EvalRO_Call struct { + *mock.Call +} + +// EvalRO is a helper method to define mock.On call +// - ctx context.Context +// - script string +// - keys []string +// - args ...interface{} +func (_e *authCodeRedisClientMock_Expecter) EvalRO(ctx interface{}, script interface{}, keys interface{}, args ...interface{}) *authCodeRedisClientMock_EvalRO_Call { + return &authCodeRedisClientMock_EvalRO_Call{Call: _e.mock.On("EvalRO", + append([]interface{}{ctx, script, keys}, args...)...)} +} + +func (_c *authCodeRedisClientMock_EvalRO_Call) Run(run func(ctx context.Context, script string, keys []string, args ...interface{})) *authCodeRedisClientMock_EvalRO_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *authCodeRedisClientMock_EvalRO_Call) Return(cmd *redis.Cmd) *authCodeRedisClientMock_EvalRO_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *authCodeRedisClientMock_EvalRO_Call) RunAndReturn(run func(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd) *authCodeRedisClientMock_EvalRO_Call { + _c.Call.Return(run) + return _c +} + +// EvalSha provides a mock function for the type authCodeRedisClientMock +func (_mock *authCodeRedisClientMock) EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, sha1, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalSha") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, sha1, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// authCodeRedisClientMock_EvalSha_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalSha' +type authCodeRedisClientMock_EvalSha_Call struct { + *mock.Call +} + +// EvalSha is a helper method to define mock.On call +// - ctx context.Context +// - sha1 string +// - keys []string +// - args ...interface{} +func (_e *authCodeRedisClientMock_Expecter) EvalSha(ctx interface{}, sha1 interface{}, keys interface{}, args ...interface{}) *authCodeRedisClientMock_EvalSha_Call { + return &authCodeRedisClientMock_EvalSha_Call{Call: _e.mock.On("EvalSha", + append([]interface{}{ctx, sha1, keys}, args...)...)} +} + +func (_c *authCodeRedisClientMock_EvalSha_Call) Run(run func(ctx context.Context, sha1 string, keys []string, args ...interface{})) *authCodeRedisClientMock_EvalSha_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *authCodeRedisClientMock_EvalSha_Call) Return(cmd *redis.Cmd) *authCodeRedisClientMock_EvalSha_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *authCodeRedisClientMock_EvalSha_Call) RunAndReturn(run func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd) *authCodeRedisClientMock_EvalSha_Call { + _c.Call.Return(run) + return _c +} + +// EvalShaRO provides a mock function for the type authCodeRedisClientMock +func (_mock *authCodeRedisClientMock) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, sha1, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalShaRO") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, sha1, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// authCodeRedisClientMock_EvalShaRO_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalShaRO' +type authCodeRedisClientMock_EvalShaRO_Call struct { + *mock.Call +} + +// EvalShaRO is a helper method to define mock.On call +// - ctx context.Context +// - sha1 string +// - keys []string +// - args ...interface{} +func (_e *authCodeRedisClientMock_Expecter) EvalShaRO(ctx interface{}, sha1 interface{}, keys interface{}, args ...interface{}) *authCodeRedisClientMock_EvalShaRO_Call { + return &authCodeRedisClientMock_EvalShaRO_Call{Call: _e.mock.On("EvalShaRO", + append([]interface{}{ctx, sha1, keys}, args...)...)} +} + +func (_c *authCodeRedisClientMock_EvalShaRO_Call) Run(run func(ctx context.Context, sha1 string, keys []string, args ...interface{})) *authCodeRedisClientMock_EvalShaRO_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *authCodeRedisClientMock_EvalShaRO_Call) Return(cmd *redis.Cmd) *authCodeRedisClientMock_EvalShaRO_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *authCodeRedisClientMock_EvalShaRO_Call) RunAndReturn(run func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd) *authCodeRedisClientMock_EvalShaRO_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type authCodeRedisClientMock +func (_mock *authCodeRedisClientMock) Get(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// authCodeRedisClientMock_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type authCodeRedisClientMock_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *authCodeRedisClientMock_Expecter) Get(ctx interface{}, key interface{}) *authCodeRedisClientMock_Get_Call { + return &authCodeRedisClientMock_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *authCodeRedisClientMock_Get_Call) Run(run func(ctx context.Context, key string)) *authCodeRedisClientMock_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *authCodeRedisClientMock_Get_Call) Return(stringCmd *redis.StringCmd) *authCodeRedisClientMock_Get_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *authCodeRedisClientMock_Get_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *authCodeRedisClientMock_Get_Call { + _c.Call.Return(run) + return _c +} + +// ScriptExists provides a mock function for the type authCodeRedisClientMock +func (_mock *authCodeRedisClientMock) ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd { + // string + _va := make([]interface{}, len(hashes)) + for _i := range hashes { + _va[_i] = hashes[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for ScriptExists") + } + + var r0 *redis.BoolSliceCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.BoolSliceCmd); ok { + r0 = returnFunc(ctx, hashes...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.BoolSliceCmd) + } + } + return r0 +} + +// authCodeRedisClientMock_ScriptExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScriptExists' +type authCodeRedisClientMock_ScriptExists_Call struct { + *mock.Call +} + +// ScriptExists is a helper method to define mock.On call +// - ctx context.Context +// - hashes ...string +func (_e *authCodeRedisClientMock_Expecter) ScriptExists(ctx interface{}, hashes ...interface{}) *authCodeRedisClientMock_ScriptExists_Call { + return &authCodeRedisClientMock_ScriptExists_Call{Call: _e.mock.On("ScriptExists", + append([]interface{}{ctx}, hashes...)...)} +} + +func (_c *authCodeRedisClientMock_ScriptExists_Call) Run(run func(ctx context.Context, hashes ...string)) *authCodeRedisClientMock_ScriptExists_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *authCodeRedisClientMock_ScriptExists_Call) Return(boolSliceCmd *redis.BoolSliceCmd) *authCodeRedisClientMock_ScriptExists_Call { + _c.Call.Return(boolSliceCmd) + return _c +} + +func (_c *authCodeRedisClientMock_ScriptExists_Call) RunAndReturn(run func(ctx context.Context, hashes ...string) *redis.BoolSliceCmd) *authCodeRedisClientMock_ScriptExists_Call { + _c.Call.Return(run) + return _c +} + +// ScriptLoad provides a mock function for the type authCodeRedisClientMock +func (_mock *authCodeRedisClientMock) ScriptLoad(ctx context.Context, script string) *redis.StringCmd { + ret := _mock.Called(ctx, script) + + if len(ret) == 0 { + panic("no return value specified for ScriptLoad") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, script) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// authCodeRedisClientMock_ScriptLoad_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScriptLoad' +type authCodeRedisClientMock_ScriptLoad_Call struct { + *mock.Call +} + +// ScriptLoad is a helper method to define mock.On call +// - ctx context.Context +// - script string +func (_e *authCodeRedisClientMock_Expecter) ScriptLoad(ctx interface{}, script interface{}) *authCodeRedisClientMock_ScriptLoad_Call { + return &authCodeRedisClientMock_ScriptLoad_Call{Call: _e.mock.On("ScriptLoad", ctx, script)} +} + +func (_c *authCodeRedisClientMock_ScriptLoad_Call) Run(run func(ctx context.Context, script string)) *authCodeRedisClientMock_ScriptLoad_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *authCodeRedisClientMock_ScriptLoad_Call) Return(stringCmd *redis.StringCmd) *authCodeRedisClientMock_ScriptLoad_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *authCodeRedisClientMock_ScriptLoad_Call) RunAndReturn(run func(ctx context.Context, script string) *redis.StringCmd) *authCodeRedisClientMock_ScriptLoad_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type authCodeRedisClientMock +func (_mock *authCodeRedisClientMock) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + ret := _mock.Called(ctx, key, value, expiration) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, interface{}, time.Duration) *redis.StatusCmd); ok { + r0 = returnFunc(ctx, key, value, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// authCodeRedisClientMock_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type authCodeRedisClientMock_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value interface{} +// - expiration time.Duration +func (_e *authCodeRedisClientMock_Expecter) Set(ctx interface{}, key interface{}, value interface{}, expiration interface{}) *authCodeRedisClientMock_Set_Call { + return &authCodeRedisClientMock_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expiration)} +} + +func (_c *authCodeRedisClientMock_Set_Call) Run(run func(ctx context.Context, key string, value interface{}, expiration time.Duration)) *authCodeRedisClientMock_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 interface{} + if args[2] != nil { + arg2 = args[2].(interface{}) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *authCodeRedisClientMock_Set_Call) Return(statusCmd *redis.StatusCmd) *authCodeRedisClientMock_Set_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *authCodeRedisClientMock_Set_Call) RunAndReturn(run func(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd) *authCodeRedisClientMock_Set_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/internal/oauth/oauth2/authz/authReqRedisClient_mock_test.go b/backend/internal/oauth/oauth2/authz/authReqRedisClient_mock_test.go new file mode 100644 index 000000000..6a652361b --- /dev/null +++ b/backend/internal/oauth/oauth2/authz/authReqRedisClient_mock_test.go @@ -0,0 +1,242 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package authz + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// newAuthReqRedisClientMock creates a new instance of authReqRedisClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newAuthReqRedisClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *authReqRedisClientMock { + mock := &authReqRedisClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// authReqRedisClientMock is an autogenerated mock type for the authReqRedisClient type +type authReqRedisClientMock struct { + mock.Mock +} + +type authReqRedisClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *authReqRedisClientMock) EXPECT() *authReqRedisClientMock_Expecter { + return &authReqRedisClientMock_Expecter{mock: &_m.Mock} +} + +// Del provides a mock function for the type authReqRedisClientMock +func (_mock *authReqRedisClientMock) Del(ctx context.Context, keys ...string) *redis.IntCmd { + // string + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Del") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// authReqRedisClientMock_Del_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Del' +type authReqRedisClientMock_Del_Call struct { + *mock.Call +} + +// Del is a helper method to define mock.On call +// - ctx context.Context +// - keys ...string +func (_e *authReqRedisClientMock_Expecter) Del(ctx interface{}, keys ...interface{}) *authReqRedisClientMock_Del_Call { + return &authReqRedisClientMock_Del_Call{Call: _e.mock.On("Del", + append([]interface{}{ctx}, keys...)...)} +} + +func (_c *authReqRedisClientMock_Del_Call) Run(run func(ctx context.Context, keys ...string)) *authReqRedisClientMock_Del_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *authReqRedisClientMock_Del_Call) Return(intCmd *redis.IntCmd) *authReqRedisClientMock_Del_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *authReqRedisClientMock_Del_Call) RunAndReturn(run func(ctx context.Context, keys ...string) *redis.IntCmd) *authReqRedisClientMock_Del_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type authReqRedisClientMock +func (_mock *authReqRedisClientMock) Get(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// authReqRedisClientMock_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type authReqRedisClientMock_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *authReqRedisClientMock_Expecter) Get(ctx interface{}, key interface{}) *authReqRedisClientMock_Get_Call { + return &authReqRedisClientMock_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *authReqRedisClientMock_Get_Call) Run(run func(ctx context.Context, key string)) *authReqRedisClientMock_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *authReqRedisClientMock_Get_Call) Return(stringCmd *redis.StringCmd) *authReqRedisClientMock_Get_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *authReqRedisClientMock_Get_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *authReqRedisClientMock_Get_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type authReqRedisClientMock +func (_mock *authReqRedisClientMock) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + ret := _mock.Called(ctx, key, value, expiration) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, interface{}, time.Duration) *redis.StatusCmd); ok { + r0 = returnFunc(ctx, key, value, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// authReqRedisClientMock_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type authReqRedisClientMock_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value interface{} +// - expiration time.Duration +func (_e *authReqRedisClientMock_Expecter) Set(ctx interface{}, key interface{}, value interface{}, expiration interface{}) *authReqRedisClientMock_Set_Call { + return &authReqRedisClientMock_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expiration)} +} + +func (_c *authReqRedisClientMock_Set_Call) Run(run func(ctx context.Context, key string, value interface{}, expiration time.Duration)) *authReqRedisClientMock_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 interface{} + if args[2] != nil { + arg2 = args[2].(interface{}) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *authReqRedisClientMock_Set_Call) Return(statusCmd *redis.StatusCmd) *authReqRedisClientMock_Set_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *authReqRedisClientMock_Set_Call) RunAndReturn(run func(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd) *authReqRedisClientMock_Set_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/internal/oauth/oauth2/authz/auth_code_redis_store.go b/backend/internal/oauth/oauth2/authz/auth_code_redis_store.go new file mode 100644 index 000000000..e2e8a23c4 --- /dev/null +++ b/backend/internal/oauth/oauth2/authz/auth_code_redis_store.go @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package authz + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/internal/system/database/provider" +) + +// consumeAuthCodeScript atomically transitions an authorization code from ACTIVE to INACTIVE. +// Returns 1 on success, 0 if not found or already consumed. +var consumeAuthCodeScript = redis.NewScript(` +local val = redis.call('GET', KEYS[1]) +if not val then return 0 end +local data = cjson.decode(val) +if data['State'] ~= ARGV[1] then return 0 end +data['State'] = ARGV[2] +redis.call('SET', KEYS[1], cjson.encode(data), 'KEEPTTL') +return 1 +`) + +// authCodeRedisClient abstracts the Redis commands used by the authorization code store. +type authCodeRedisClient interface { + redis.Scripter + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd + Get(ctx context.Context, key string) *redis.StringCmd +} + +// redisAuthorizationCodeStore is the Redis-backed implementation of AuthorizationCodeStoreInterface. +type redisAuthorizationCodeStore struct { + client authCodeRedisClient + keyPrefix string + deploymentID string +} + +// newRedisAuthorizationCodeStore creates a new Redis-backed authorization code store. +func newRedisAuthorizationCodeStore(p provider.RedisProviderInterface) AuthorizationCodeStoreInterface { + return &redisAuthorizationCodeStore{ + client: p.GetRedisClient(), + keyPrefix: p.GetKeyPrefix(), + deploymentID: config.GetThunderRuntime().Config.Server.Identifier, + } +} + +// authCodeKey builds the Redis key for an authorization code. +func (s *redisAuthorizationCodeStore) authCodeKey(code string) string { + return fmt.Sprintf("%s:runtime:%s:authcode:%s", s.keyPrefix, s.deploymentID, code) +} + +// InsertAuthorizationCode serializes the authorization code and stores it in Redis with a TTL. +func (s *redisAuthorizationCodeStore) InsertAuthorizationCode( + ctx context.Context, authzCode AuthorizationCode, +) error { + data, err := json.Marshal(authzCode) + if err != nil { + return fmt.Errorf("failed to marshal authorization code: %w", err) + } + + ttl := time.Until(authzCode.ExpiryTime) + if ttl <= 0 { + return fmt.Errorf("authorization code already expired") + } + if err := s.client.Set(ctx, s.authCodeKey(authzCode.Code), data, ttl).Err(); err != nil { + return fmt.Errorf("failed to store authorization code in Redis: %w", err) + } + + return nil +} + +// ConsumeAuthorizationCode atomically transitions an ACTIVE code to INACTIVE. +// Returns true if consumed, false if not found or already consumed. +func (s *redisAuthorizationCodeStore) ConsumeAuthorizationCode( + ctx context.Context, authCode string, +) (bool, error) { + n, err := consumeAuthCodeScript.Run(ctx, s.client, []string{s.authCodeKey(authCode)}, + AuthCodeStateActive, AuthCodeStateInactive).Int() + if err != nil && !errors.Is(err, redis.Nil) { + return false, fmt.Errorf("failed to consume authorization code: %w", err) + } + return n == 1, nil +} + +// GetAuthorizationCode retrieves an authorization code by code value. +func (s *redisAuthorizationCodeStore) GetAuthorizationCode( + ctx context.Context, authCode string, +) (*AuthorizationCode, error) { + data, err := s.client.Get(ctx, s.authCodeKey(authCode)).Bytes() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, errAuthorizationCodeNotFound + } + return nil, fmt.Errorf("failed to get authorization code from Redis: %w", err) + } + + var result AuthorizationCode + if err := json.Unmarshal(data, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal authorization code: %w", err) + } + + return &result, nil +} diff --git a/backend/internal/oauth/oauth2/authz/auth_code_redis_store_test.go b/backend/internal/oauth/oauth2/authz/auth_code_redis_store_test.go new file mode 100644 index 000000000..d5a6f87f3 --- /dev/null +++ b/backend/internal/oauth/oauth2/authz/auth_code_redis_store_test.go @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package authz + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +const ( + redisTestKeyPrefix = "thunder" + redisTestDeploymentID = "test-redis-deployment" + redisTestAuthCode = "test-auth-code" +) + +type RedisAuthorizationCodeStoreTestSuite struct { + suite.Suite + store *redisAuthorizationCodeStore + mockClient *authCodeRedisClientMock + ctx context.Context + authCode AuthorizationCode + redisKey string +} + +func TestRedisAuthorizationCodeStoreTestSuite(t *testing.T) { + suite.Run(t, new(RedisAuthorizationCodeStoreTestSuite)) +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) SetupTest() { + suite.mockClient = newAuthCodeRedisClientMock(suite.T()) + suite.ctx = context.Background() + suite.store = &redisAuthorizationCodeStore{ + client: suite.mockClient, + keyPrefix: redisTestKeyPrefix, + deploymentID: redisTestDeploymentID, + } + suite.authCode = AuthorizationCode{ + CodeID: "test-code-id", + Code: redisTestAuthCode, + ClientID: "test-client-id", + RedirectURI: "https://client.example.com/callback", + AuthorizedUserID: "test-user-id", + TimeCreated: time.Now(), + ExpiryTime: time.Now().Add(10 * time.Minute), + Scopes: "read write", + State: AuthCodeStateActive, + } + suite.redisKey = fmt.Sprintf("%s:runtime:%s:authcode:%s", + redisTestKeyPrefix, redisTestDeploymentID, redisTestAuthCode) +} + +// Tests for authCodeKey + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestAuthCodeKey() { + key := suite.store.authCodeKey(redisTestAuthCode) + suite.Equal(suite.redisKey, key) +} + +// Tests for InsertAuthorizationCode + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestInsertAuthorizationCode_Success() { + statusCmd := redis.NewStatusCmd(suite.ctx) + // TTL is time.Until(expiry) which changes slightly; accept any positive duration. + suite.mockClient.On("Set", suite.ctx, suite.redisKey, mock.Anything, + mock.MatchedBy(func(d time.Duration) bool { return d > 0 })).Return(statusCmd) + + err := suite.store.InsertAuthorizationCode(suite.ctx, suite.authCode) + suite.NoError(err) +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestInsertAuthorizationCode_AlreadyExpired() { + expiredCode := suite.authCode + expiredCode.ExpiryTime = time.Now().Add(-1 * time.Minute) + + err := suite.store.InsertAuthorizationCode(suite.ctx, expiredCode) + suite.Error(err) + suite.Contains(err.Error(), "authorization code already expired") +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestInsertAuthorizationCode_SetError() { + statusCmd := redis.NewStatusCmd(suite.ctx) + statusCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Set", suite.ctx, suite.redisKey, mock.Anything, + mock.MatchedBy(func(d time.Duration) bool { return d > 0 })).Return(statusCmd) + + err := suite.store.InsertAuthorizationCode(suite.ctx, suite.authCode) + suite.Error(err) + suite.Contains(err.Error(), "failed to store authorization code in Redis") +} + +// Tests for GetAuthorizationCode + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestGetAuthorizationCode_Success() { + data, _ := json.Marshal(suite.authCode) + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal(string(data)) + suite.mockClient.On("Get", suite.ctx, suite.redisKey).Return(stringCmd) + + result, err := suite.store.GetAuthorizationCode(suite.ctx, redisTestAuthCode) + suite.NoError(err) + suite.NotNil(result) + suite.Equal(suite.authCode.CodeID, result.CodeID) + suite.Equal(suite.authCode.Code, result.Code) + suite.Equal(suite.authCode.ClientID, result.ClientID) +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestGetAuthorizationCode_NotFound() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetErr(redis.Nil) + suite.mockClient.On("Get", suite.ctx, suite.redisKey).Return(stringCmd) + + result, err := suite.store.GetAuthorizationCode(suite.ctx, redisTestAuthCode) + suite.Error(err) + suite.Equal(errAuthorizationCodeNotFound, err) + suite.Nil(result) +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestGetAuthorizationCode_GetError() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Get", suite.ctx, suite.redisKey).Return(stringCmd) + + result, err := suite.store.GetAuthorizationCode(suite.ctx, redisTestAuthCode) + suite.Error(err) + suite.Contains(err.Error(), "failed to get authorization code from Redis") + suite.Nil(result) +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestGetAuthorizationCode_UnmarshalError() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal("not valid json{{{") + suite.mockClient.On("Get", suite.ctx, suite.redisKey).Return(stringCmd) + + result, err := suite.store.GetAuthorizationCode(suite.ctx, redisTestAuthCode) + suite.Error(err) + suite.Contains(err.Error(), "failed to unmarshal authorization code") + suite.Nil(result) +} + +// Tests for ConsumeAuthorizationCode +// +// consumeAuthCodeScript.Run() calls EvalSha with the script's precomputed SHA. +// The Lua script returns 1 when consumed, 0 when not found or already consumed. + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestConsumeAuthorizationCode_Success() { + cmd := redis.NewCmd(suite.ctx) + cmd.SetVal(int64(1)) + suite.mockClient.On("EvalSha", suite.ctx, consumeAuthCodeScript.Hash(), + []string{suite.redisKey}, AuthCodeStateActive, AuthCodeStateInactive).Return(cmd) + + consumed, err := suite.store.ConsumeAuthorizationCode(suite.ctx, redisTestAuthCode) + suite.NoError(err) + suite.True(consumed) +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestConsumeAuthorizationCode_AlreadyConsumed() { + cmd := redis.NewCmd(suite.ctx) + cmd.SetVal(int64(0)) + suite.mockClient.On("EvalSha", suite.ctx, consumeAuthCodeScript.Hash(), + []string{suite.redisKey}, AuthCodeStateActive, AuthCodeStateInactive).Return(cmd) + + consumed, err := suite.store.ConsumeAuthorizationCode(suite.ctx, redisTestAuthCode) + suite.NoError(err) + suite.False(consumed) +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestConsumeAuthorizationCode_RedisNil_TreatedAsNotConsumed() { + // redis.Nil is treated as "not consumed" rather than an error. + cmd := redis.NewCmd(suite.ctx) + cmd.SetErr(redis.Nil) + suite.mockClient.On("EvalSha", suite.ctx, consumeAuthCodeScript.Hash(), + []string{suite.redisKey}, AuthCodeStateActive, AuthCodeStateInactive).Return(cmd) + + consumed, err := suite.store.ConsumeAuthorizationCode(suite.ctx, redisTestAuthCode) + suite.NoError(err) + suite.False(consumed) +} + +func (suite *RedisAuthorizationCodeStoreTestSuite) TestConsumeAuthorizationCode_ScriptError() { + cmd := redis.NewCmd(suite.ctx) + cmd.SetErr(errors.New("WRONGTYPE Operation against a key holding the wrong kind of value")) + suite.mockClient.On("EvalSha", suite.ctx, consumeAuthCodeScript.Hash(), + []string{suite.redisKey}, AuthCodeStateActive, AuthCodeStateInactive).Return(cmd) + + consumed, err := suite.store.ConsumeAuthorizationCode(suite.ctx, redisTestAuthCode) + suite.Error(err) + suite.Contains(err.Error(), "failed to consume authorization code") + suite.False(consumed) +} diff --git a/backend/internal/oauth/oauth2/authz/auth_code_store_test.go b/backend/internal/oauth/oauth2/authz/auth_code_store_test.go index bf50dfa74..0fe279dfa 100644 --- a/backend/internal/oauth/oauth2/authz/auth_code_store_test.go +++ b/backend/internal/oauth/oauth2/authz/auth_code_store_test.go @@ -52,12 +52,12 @@ func (suite *AuthorizationCodeStoreTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } diff --git a/backend/internal/oauth/oauth2/authz/auth_req_redis_store.go b/backend/internal/oauth/oauth2/authz/auth_req_redis_store.go new file mode 100644 index 000000000..bd788b48e --- /dev/null +++ b/backend/internal/oauth/oauth2/authz/auth_req_redis_store.go @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package authz + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/internal/system/database/provider" + "github.com/asgardeo/thunder/internal/system/utils" +) + +// authReqRedisClient abstracts the Redis commands used by the authorization request store. +type authReqRedisClient interface { + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd + Get(ctx context.Context, key string) *redis.StringCmd + Del(ctx context.Context, keys ...string) *redis.IntCmd +} + +// redisAuthorizationRequestStore is the Redis-backed implementation of authorizationRequestStoreInterface. +type redisAuthorizationRequestStore struct { + client authReqRedisClient + keyPrefix string + deploymentID string + validityPeriod time.Duration +} + +// newRedisAuthorizationRequestStore creates a new Redis-backed authorization request store. +func newRedisAuthorizationRequestStore(p provider.RedisProviderInterface) authorizationRequestStoreInterface { + return &redisAuthorizationRequestStore{ + client: p.GetRedisClient(), + keyPrefix: p.GetKeyPrefix(), + deploymentID: config.GetThunderRuntime().Config.Server.Identifier, + validityPeriod: 10 * time.Minute, + } +} + +// authReqKey builds the Redis key for an authorization request. +func (s *redisAuthorizationRequestStore) authReqKey(key string) string { + return fmt.Sprintf("%s:runtime:%s:authreq:%s", s.keyPrefix, s.deploymentID, key) +} + +// AddRequest adds an authorization request context entry to Redis with a TTL. +func (s *redisAuthorizationRequestStore) AddRequest(ctx context.Context, value authRequestContext) (string, error) { + key, err := utils.GenerateUUIDv7() + if err != nil { + return "", fmt.Errorf("failed to generate UUID: %w", err) + } + + data, err := json.Marshal(value) + if err != nil { + return "", fmt.Errorf("failed to marshal request context: %w", err) + } + + if err := s.client.Set(ctx, s.authReqKey(key), data, s.validityPeriod).Err(); err != nil { + return "", fmt.Errorf("failed to store authorization request in Redis: %w", err) + } + + return key, nil +} + +// GetRequest retrieves an authorization request context entry from Redis. +func (s *redisAuthorizationRequestStore) GetRequest( + ctx context.Context, key string, +) (bool, authRequestContext, error) { + if key == "" { + return false, authRequestContext{}, nil + } + + data, err := s.client.Get(ctx, s.authReqKey(key)).Bytes() + if err != nil { + if errors.Is(err, redis.Nil) { + return false, authRequestContext{}, nil + } + return false, authRequestContext{}, fmt.Errorf("failed to get authorization request from Redis: %w", err) + } + + var result authRequestContext + if err := json.Unmarshal(data, &result); err != nil { + return false, authRequestContext{}, fmt.Errorf("failed to unmarshal authorization request: %w", err) + } + + return true, result, nil +} + +// ClearRequest removes a specific authorization request context entry from Redis. +func (s *redisAuthorizationRequestStore) ClearRequest(ctx context.Context, key string) error { + if key == "" { + return nil + } + + if err := s.client.Del(ctx, s.authReqKey(key)).Err(); err != nil { + return fmt.Errorf("failed to delete authorization request from Redis: %w", err) + } + + return nil +} diff --git a/backend/internal/oauth/oauth2/authz/auth_req_redis_store_test.go b/backend/internal/oauth/oauth2/authz/auth_req_redis_store_test.go new file mode 100644 index 000000000..e7d7dd8c7 --- /dev/null +++ b/backend/internal/oauth/oauth2/authz/auth_req_redis_store_test.go @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package authz + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/asgardeo/thunder/internal/oauth/oauth2/model" +) + +const redisTestReqKey = "test-req-key" + +type RedisAuthorizationRequestStoreTestSuite struct { + suite.Suite + store *redisAuthorizationRequestStore + mockClient *authReqRedisClientMock + ctx context.Context + authReq authRequestContext + redisKey string +} + +func TestRedisAuthorizationRequestStoreTestSuite(t *testing.T) { + suite.Run(t, new(RedisAuthorizationRequestStoreTestSuite)) +} + +func (suite *RedisAuthorizationRequestStoreTestSuite) SetupTest() { + suite.mockClient = newAuthReqRedisClientMock(suite.T()) + suite.ctx = context.Background() + suite.store = &redisAuthorizationRequestStore{ + client: suite.mockClient, + keyPrefix: redisTestKeyPrefix, + deploymentID: redisTestDeploymentID, + validityPeriod: 10 * time.Minute, + } + suite.authReq = authRequestContext{ + OAuthParameters: model.OAuthParameters{ + ClientID: "test-client-id", + RedirectURI: "https://client.example.com/callback", + }, + } + suite.redisKey = fmt.Sprintf("%s:runtime:%s:authreq:%s", + redisTestKeyPrefix, redisTestDeploymentID, redisTestReqKey) +} + +// Tests for authReqKey + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestAuthReqKey() { + key := suite.store.authReqKey(redisTestReqKey) + suite.Equal(suite.redisKey, key) +} + +// Tests for AddRequest + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestAddRequest_Success() { + statusCmd := redis.NewStatusCmd(suite.ctx) + // The key is a dynamically generated UUID — match any non-empty string. + suite.mockClient.On("Set", suite.ctx, + mock.MatchedBy(func(k string) bool { return k != "" }), + mock.Anything, suite.store.validityPeriod).Return(statusCmd) + + key, err := suite.store.AddRequest(suite.ctx, suite.authReq) + suite.NoError(err) + suite.NotEmpty(key) +} + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestAddRequest_SetError() { + statusCmd := redis.NewStatusCmd(suite.ctx) + statusCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Set", suite.ctx, + mock.MatchedBy(func(k string) bool { return k != "" }), + mock.Anything, suite.store.validityPeriod).Return(statusCmd) + + key, err := suite.store.AddRequest(suite.ctx, suite.authReq) + suite.Error(err) + suite.Contains(err.Error(), "failed to store authorization request in Redis") + suite.Empty(key) +} + +// Tests for GetRequest + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestGetRequest_Success() { + data, _ := json.Marshal(suite.authReq) + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal(string(data)) + suite.mockClient.On("Get", suite.ctx, suite.redisKey).Return(stringCmd) + + found, result, err := suite.store.GetRequest(suite.ctx, redisTestReqKey) + suite.NoError(err) + suite.True(found) + suite.Equal(suite.authReq.OAuthParameters.ClientID, result.OAuthParameters.ClientID) +} + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestGetRequest_EmptyKey() { + found, result, err := suite.store.GetRequest(suite.ctx, "") + suite.NoError(err) + suite.False(found) + suite.Equal(authRequestContext{}, result) +} + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestGetRequest_NotFound() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetErr(redis.Nil) + suite.mockClient.On("Get", suite.ctx, suite.redisKey).Return(stringCmd) + + found, result, err := suite.store.GetRequest(suite.ctx, redisTestReqKey) + suite.NoError(err) + suite.False(found) + suite.Equal(authRequestContext{}, result) +} + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestGetRequest_GetError() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Get", suite.ctx, suite.redisKey).Return(stringCmd) + + found, result, err := suite.store.GetRequest(suite.ctx, redisTestReqKey) + suite.Error(err) + suite.Contains(err.Error(), "failed to get authorization request from Redis") + suite.False(found) + suite.Equal(authRequestContext{}, result) +} + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestGetRequest_UnmarshalError() { + stringCmd := redis.NewStringCmd(suite.ctx) + stringCmd.SetVal("not valid json{{{") + suite.mockClient.On("Get", suite.ctx, suite.redisKey).Return(stringCmd) + + found, result, err := suite.store.GetRequest(suite.ctx, redisTestReqKey) + suite.Error(err) + suite.Contains(err.Error(), "failed to unmarshal authorization request") + suite.False(found) + suite.Equal(authRequestContext{}, result) +} + +// Tests for ClearRequest + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestClearRequest_Success() { + intCmd := redis.NewIntCmd(suite.ctx) + intCmd.SetVal(1) + suite.mockClient.On("Del", suite.ctx, suite.redisKey).Return(intCmd) + + err := suite.store.ClearRequest(suite.ctx, redisTestReqKey) + suite.NoError(err) +} + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestClearRequest_EmptyKey() { + err := suite.store.ClearRequest(suite.ctx, "") + suite.NoError(err) +} + +func (suite *RedisAuthorizationRequestStoreTestSuite) TestClearRequest_DelError() { + intCmd := redis.NewIntCmd(suite.ctx) + intCmd.SetErr(errors.New("connection refused")) + suite.mockClient.On("Del", suite.ctx, suite.redisKey).Return(intCmd) + + err := suite.store.ClearRequest(suite.ctx, redisTestReqKey) + suite.Error(err) + suite.Contains(err.Error(), "failed to delete authorization request from Redis") +} diff --git a/backend/internal/oauth/oauth2/authz/auth_req_store_test.go b/backend/internal/oauth/oauth2/authz/auth_req_store_test.go index ad0386fe3..9cbccb015 100644 --- a/backend/internal/oauth/oauth2/authz/auth_req_store_test.go +++ b/backend/internal/oauth/oauth2/authz/auth_req_store_test.go @@ -51,12 +51,12 @@ func (suite *AuthorizationRequestStoreTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } diff --git a/backend/internal/oauth/oauth2/authz/handler_test.go b/backend/internal/oauth/oauth2/authz/handler_test.go index 1dfdc5c02..16e85897a 100644 --- a/backend/internal/oauth/oauth2/authz/handler_test.go +++ b/backend/internal/oauth/oauth2/authz/handler_test.go @@ -62,12 +62,12 @@ func (suite *AuthorizeHandlerTestSuite) BeforeTest(suiteName, testName string) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, OAuth: config.OAuthConfig{ diff --git a/backend/internal/oauth/oauth2/authz/init.go b/backend/internal/oauth/oauth2/authz/init.go index 3d7311c0d..a94d9b96e 100644 --- a/backend/internal/oauth/oauth2/authz/init.go +++ b/backend/internal/oauth/oauth2/authz/init.go @@ -24,10 +24,12 @@ import ( "github.com/asgardeo/thunder/internal/application" "github.com/asgardeo/thunder/internal/flow/flowexec" + "github.com/asgardeo/thunder/internal/system/config" "github.com/asgardeo/thunder/internal/system/constants" "github.com/asgardeo/thunder/internal/system/database/provider" "github.com/asgardeo/thunder/internal/system/jose/jwt" "github.com/asgardeo/thunder/internal/system/middleware" + "github.com/asgardeo/thunder/internal/system/transaction" ) // Initialize initializes the authorization handler and registers its routes. @@ -37,16 +39,11 @@ func Initialize( jwtService jwt.JWTServiceInterface, flowExecService flowexec.FlowExecServiceInterface, ) (AuthorizeServiceInterface, error) { - authzCodeStore := initializeAuthorizationCodeStore() - - dbProvider := provider.GetDBProvider() - transactioner, err := dbProvider.GetRuntimeDBTransactioner() + authzCodeStore, authzReqStore, transactioner, err := initializeAuthorizationStores() if err != nil { - return nil, fmt.Errorf("failed to get runtime DB transactioner: %w", err) + return nil, fmt.Errorf("failed to initialize authorization stores: %w", err) } - authzReqStore := newAuthorizationRequestStore() - authzService := newAuthorizeService( applicationService, jwtService, flowExecService, authzCodeStore, authzReqStore, transactioner, ) @@ -55,9 +52,22 @@ func Initialize( return authzService, nil } -// initializeAuthorizationCodeStore creates the authorization code store. -func initializeAuthorizationCodeStore() AuthorizationCodeStoreInterface { - return newAuthorizationCodeStore() +// initializeAuthorizationStores creates the authorization code store, request store, and transactioner. +func initializeAuthorizationStores() ( + AuthorizationCodeStoreInterface, authorizationRequestStoreInterface, transaction.Transactioner, error) { + if config.GetThunderRuntime().Config.Database.Runtime.Type == provider.DataSourceTypeRedis { + redisProvider := provider.GetRedisProvider() + return newRedisAuthorizationCodeStore(redisProvider), + newRedisAuthorizationRequestStore(redisProvider), + transaction.NewNoOpTransactioner(), + nil + } + dbProvider := provider.GetDBProvider() + transactioner, err := dbProvider.GetRuntimeDBTransactioner() + if err != nil { + return nil, nil, nil, err + } + return newAuthorizationCodeStore(), newAuthorizationRequestStore(), transactioner, nil } // registerRoutes registers the routes for OAuth2 authorization operations. diff --git a/backend/internal/oauth/oauth2/authz/init_test.go b/backend/internal/oauth/oauth2/authz/init_test.go index 105a80894..c9063af1c 100644 --- a/backend/internal/oauth/oauth2/authz/init_test.go +++ b/backend/internal/oauth/oauth2/authz/init_test.go @@ -49,12 +49,12 @@ func (suite *InitTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: "thunder_test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "thunder_test.db"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: "thunder_test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "thunder_test.db"}, }, }, GateClient: config.GateClientConfig{ diff --git a/backend/internal/oauth/oauth2/authz/service_test.go b/backend/internal/oauth/oauth2/authz/service_test.go index 6d151882b..a2c756483 100644 --- a/backend/internal/oauth/oauth2/authz/service_test.go +++ b/backend/internal/oauth/oauth2/authz/service_test.go @@ -82,8 +82,8 @@ func (suite *AuthorizeServiceTestSuite) BeforeTest(suiteName, testName string) { ErrorPath: "/error", }, Database: config.DatabaseConfig{ - Config: config.DataSource{Type: "sqlite", Path: ":memory:"}, - Runtime: config.DataSource{Type: "sqlite", Path: ":memory:"}, + Config: config.DataSource{Type: "sqlite", SQLite: config.SQLiteDataSource{Path: ":memory:"}}, + Runtime: config.DataSource{Type: "sqlite", SQLite: config.SQLiteDataSource{Path: ":memory:"}}, }, OAuth: config.OAuthConfig{ AuthorizationCode: config.AuthorizationCodeConfig{ValidityPeriod: 600}, diff --git a/backend/internal/oauth/oauth2/dcr/init_test.go b/backend/internal/oauth/oauth2/dcr/init_test.go index 8f04b8ed3..bafc45663 100644 --- a/backend/internal/oauth/oauth2/dcr/init_test.go +++ b/backend/internal/oauth/oauth2/dcr/init_test.go @@ -47,9 +47,9 @@ func (suite *InitTestSuite) SetupTest() { suite.mockOUService = oumock.NewOrganizationUnitServiceInterfaceMock(suite.T()) testConfig := &config.Config{ Database: config.DatabaseConfig{ - Config: config.DataSource{Type: "sqlite", Path: "thunder_test.db"}, - Runtime: config.DataSource{Type: "sqlite", Path: "thunder_test.db"}, - User: config.DataSource{Type: "sqlite", Path: "thunder_test.db"}, + Config: config.DataSource{Type: "sqlite", SQLite: config.SQLiteDataSource{Path: "thunder_test.db"}}, + Runtime: config.DataSource{Type: "sqlite", SQLite: config.SQLiteDataSource{Path: "thunder_test.db"}}, + User: config.DataSource{Type: "sqlite", SQLite: config.SQLiteDataSource{Path: "thunder_test.db"}}, }, } _ = config.InitializeThunderRuntime("", testConfig) diff --git a/backend/internal/ou/init_test.go b/backend/internal/ou/init_test.go index 6f2ce327a..b0de6a606 100644 --- a/backend/internal/ou/init_test.go +++ b/backend/internal/ou/init_test.go @@ -47,8 +47,8 @@ func (suite *InitTestSuite) SetupTest() { }, Database: config.DatabaseConfig{ User: config.DataSource{ - Type: "sqlite", - Path: "test.db", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: "test.db"}, }, }, } diff --git a/backend/internal/resource/init_test.go b/backend/internal/resource/init_test.go index 7856de5dd..61dbd28ee 100644 --- a/backend/internal/resource/init_test.go +++ b/backend/internal/resource/init_test.go @@ -53,16 +53,16 @@ func (suite *InitTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, User: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ diff --git a/backend/internal/resource/service_test.go b/backend/internal/resource/service_test.go index 2ee271e82..fe0fd592d 100644 --- a/backend/internal/resource/service_test.go +++ b/backend/internal/resource/service_test.go @@ -99,12 +99,12 @@ func (suite *ResourceServiceTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -135,12 +135,12 @@ func (suite *ResourceServiceTestSuite) TestNewResourceService_InvalidDelimiter() testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ @@ -4165,12 +4165,12 @@ func (suite *ResourceServiceTestSuite) TestValidatePermissions() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Server: config.ServerConfig{ diff --git a/backend/internal/system/config/config.go b/backend/internal/system/config/config.go index 8fc2a0143..4a986adfd 100644 --- a/backend/internal/system/config/config.go +++ b/backend/internal/system/config/config.go @@ -61,14 +61,30 @@ type TLSConfig struct { } // DataSource holds the individual database connection details. +// Type is the only common field; connection parameters live under the +// matching sub-struct (Postgres, SQLite, or Redis). type DataSource struct { - Type string `yaml:"type" json:"type"` + Type string `yaml:"type" json:"type"` + Postgres PostgresDataSource `yaml:"postgres" json:"postgres"` + SQLite SQLiteDataSource `yaml:"sqlite" json:"sqlite"` + Redis RedisDataSource `yaml:"redis" json:"redis"` +} + +// PostgresDataSource holds PostgreSQL-specific connection details. +type PostgresDataSource struct { Hostname string `yaml:"hostname" json:"hostname"` Port int `yaml:"port" json:"port"` Name string `yaml:"name" json:"name"` Username string `yaml:"username" json:"username"` Password string `yaml:"password" json:"password"` SSLMode string `yaml:"sslmode" json:"sslmode"` + MaxOpenConns int `yaml:"max_open_conns" json:"max_open_conns"` + MaxIdleConns int `yaml:"max_idle_conns" json:"max_idle_conns"` + ConnMaxLifetime int `yaml:"conn_max_lifetime" json:"conn_max_lifetime"` +} + +// SQLiteDataSource holds SQLite-specific connection details. +type SQLiteDataSource struct { Path string `yaml:"path" json:"path"` Options string `yaml:"options" json:"options"` MaxOpenConns int `yaml:"max_open_conns" json:"max_open_conns"` @@ -76,6 +92,15 @@ type DataSource struct { ConnMaxLifetime int `yaml:"conn_max_lifetime" json:"conn_max_lifetime"` } +// RedisDataSource holds Redis-specific connection details. +type RedisDataSource struct { + Address string `yaml:"address" json:"address"` + Username string `yaml:"username" json:"username"` + Password string `yaml:"password" json:"password"` + DB int `yaml:"db" json:"db"` + KeyPrefix string `yaml:"key_prefix" json:"key_prefix"` +} + // DatabaseConfig holds the different database configuration details. type DatabaseConfig struct { Config DataSource `yaml:"config" json:"config"` diff --git a/backend/internal/system/config/config_test.go b/backend/internal/system/config/config_test.go index f70ae94a6..0a103b2c8 100644 --- a/backend/internal/system/config/config_test.go +++ b/backend/internal/system/config/config_test.go @@ -143,7 +143,9 @@ server: port: 8090 database: config: - password: "{{.TestVar}}" + type: "sqlite" + sqlite: + path: "{{.TestVar}}" ` tempDir := suite.T().TempDir() @@ -160,7 +162,7 @@ database: assert.Equal(suite.T(), "user-host", config.Server.Hostname) assert.Equal(suite.T(), 8090, config.Server.Port) assert.Equal(suite.T(), false, config.Server.HTTPOnly) // Zero value for bool - assert.Equal(suite.T(), "mysql", config.Database.Config.Password) + assert.Equal(suite.T(), "mysql", config.Database.Config.SQLite.Path) } func (suite *ConfigTestSuite) TestLoadConfigWithDefaults_ErrorCases() { @@ -217,14 +219,18 @@ func (suite *ConfigTestSuite) TestMergeStructs() { }, Database: DatabaseConfig{ Config: DataSource{ - Type: "postgres", - Hostname: "base-config-host", - Port: 5432, + Type: "postgres", + Postgres: PostgresDataSource{ + Hostname: "base-config-host", + Port: 5432, + }, }, Runtime: DataSource{ - Type: "postgres", - Hostname: "base-runtime-host", - Port: 5432, + Type: "postgres", + Postgres: PostgresDataSource{ + Hostname: "base-runtime-host", + Port: 5432, + }, }, }, } @@ -256,7 +262,9 @@ func (suite *ConfigTestSuite) TestMergeStructs() { }, Database: DatabaseConfig{ Config: DataSource{ - Username: "user-config-username", // Override + Postgres: PostgresDataSource{ + Username: "user-config-username", // Override + }, // Other fields are zero values, should not override }, }, @@ -286,9 +294,9 @@ func (suite *ConfigTestSuite) TestMergeStructs() { assert.Equal(suite.T(), 600, base.Cache.Properties[0].TTL) // Test nested struct field override - assert.Equal(suite.T(), "user-config-username", base.Database.Config.Username) - assert.Equal(suite.T(), "postgres", base.Database.Config.Type) // Not overridden (zero value) - assert.Equal(suite.T(), "base-config-host", base.Database.Config.Hostname) // Not overridden (zero value) + assert.Equal(suite.T(), "user-config-username", base.Database.Config.Postgres.Username) + assert.Equal(suite.T(), "postgres", base.Database.Config.Type) // Not overridden (zero value) + assert.Equal(suite.T(), "base-config-host", base.Database.Config.Postgres.Hostname) // Not overridden (zero value) } func (suite *ConfigTestSuite) TestMergeStructs_EdgeCases() { diff --git a/backend/internal/system/database/provider/dbprovider.go b/backend/internal/system/database/provider/dbprovider.go index e37a7f61d..c4d3fd9a0 100644 --- a/backend/internal/system/database/provider/dbprovider.go +++ b/backend/internal/system/database/provider/dbprovider.go @@ -134,8 +134,12 @@ func (d *dbProvider) GetUserDBTransactioner() (transaction.Transactioner, error) } // GetRuntimeDBTransactioner returns a transactioner for the runtime database. -// The transactioner manages database transactions with automatic nesting detection. func (d *dbProvider) GetRuntimeDBTransactioner() (transaction.Transactioner, error) { + // When the runtime store is Redis, a no-op transactioner is returned since Redis does + // not support SQL-style transactions. + if config.GetThunderRuntime().Config.Database.Runtime.Type == DataSourceTypeRedis { + return transaction.NewNoOpTransactioner(), nil + } return d.getTransactioner(d.GetRuntimeDBClient, dbNameRuntime) } @@ -163,9 +167,11 @@ func (d *dbProvider) initializeAllClients() { } runtimeDBConfig := config.GetThunderRuntime().Config.Database.Runtime - err = d.initializeClient(&d.runtimeClient, runtimeDBConfig, dbNameRuntime) - if err != nil { - logger.Error("Failed to initialize runtime database client", log.Error(err)) + if runtimeDBConfig.Type != DataSourceTypeRedis { + err = d.initializeClient(&d.runtimeClient, runtimeDBConfig, dbNameRuntime) + if err != nil { + logger.Error("Failed to initialize runtime database client", log.Error(err)) + } } userDBConfig := config.GetThunderRuntime().Config.Database.User @@ -186,6 +192,10 @@ func (d *dbProvider) getOrInitClient( if dataSource.Type == "" { return nil, fmt.Errorf("database type is not configured") } + // Redis runtime stores bypass the SQL client entirely + if dataSource.Type == DataSourceTypeRedis { + return nil, fmt.Errorf("runtime database is configured as Redis; use RedisProvider instead") + } mutex.RLock() if *clientPtr != nil { @@ -218,10 +228,21 @@ func (d *dbProvider) initializeClient(clientPtr *DBClientInterface, dataSource c return fmt.Errorf("failed to connect to database %s: %w", dbName, err) } - // Configure connection pool using values from configuration - db.SetMaxOpenConns(dataSource.MaxOpenConns) - db.SetMaxIdleConns(dataSource.MaxIdleConns) - db.SetConnMaxLifetime(time.Duration(dataSource.ConnMaxLifetime) * time.Second) + // Configure connection pool using values from the type-specific sub-config. + var maxOpenConns, maxIdleConns, connMaxLifetime int + switch dataSource.Type { + case dataSourceTypePostgres: + maxOpenConns = dataSource.Postgres.MaxOpenConns + maxIdleConns = dataSource.Postgres.MaxIdleConns + connMaxLifetime = dataSource.Postgres.ConnMaxLifetime + case dataSourceTypeSQLite: + maxOpenConns = dataSource.SQLite.MaxOpenConns + maxIdleConns = dataSource.SQLite.MaxIdleConns + connMaxLifetime = dataSource.SQLite.ConnMaxLifetime + } + db.SetMaxOpenConns(maxOpenConns) + db.SetMaxIdleConns(maxIdleConns) + db.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Second) // Test the database connection. if err := db.Ping(); err != nil { @@ -253,17 +274,18 @@ func (d *dbProvider) getDBConfig(dataSource config.DataSource) dbConfig { switch dataSource.Type { case dataSourceTypePostgres: + pg := dataSource.Postgres dbConfig.driverName = dataSourceTypePostgres dbConfig.dsn = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", - dataSource.Hostname, dataSource.Port, dataSource.Username, dataSource.Password, - dataSource.Name, dataSource.SSLMode) + pg.Hostname, pg.Port, pg.Username, pg.Password, pg.Name, pg.SSLMode) case dataSourceTypeSQLite: + sl := dataSource.SQLite dbConfig.driverName = dataSourceTypeSQLite - options := dataSource.Options + options := sl.Options if options != "" && options[0] != '?' { options = "?" + options } - dbConfig.dsn = fmt.Sprintf("%s%s", path.Join(config.GetThunderRuntime().ThunderHome, dataSource.Path), options) + dbConfig.dsn = fmt.Sprintf("%s%s", path.Join(config.GetThunderRuntime().ThunderHome, sl.Path), options) } return dbConfig @@ -277,7 +299,14 @@ func (d *dbProvider) Close() error { configErr := d.closeClient(&d.configClient, &d.configMutex, "config") runtimeErr := d.closeClient(&d.runtimeClient, &d.runtimeMutex, "runtime") userErr := d.closeClient(&d.userClient, &d.userMutex, "user") - return errors.Join(configErr, runtimeErr, userErr) + + // Close the Redis runtime provider if it was initialized. + var redisErr error + if redisInstance != nil { + redisErr = redisInstance.Close() + } + + return errors.Join(configErr, runtimeErr, userErr, redisErr) } // closeClient is a helper to close a DB client with locking. diff --git a/backend/internal/system/database/provider/dbprovider_test.go b/backend/internal/system/database/provider/dbprovider_test.go index 88cff948f..e5b666853 100644 --- a/backend/internal/system/database/provider/dbprovider_test.go +++ b/backend/internal/system/database/provider/dbprovider_test.go @@ -48,9 +48,9 @@ func (suite *DBProviderTestSuite) SetupTest() { // Initialize a dummy config dummyConfig := &config.Config{ Database: config.DatabaseConfig{ - Config: config.DataSource{Name: "identity", Type: "postgres"}, - Runtime: config.DataSource{Name: "runtime", Type: "postgres"}, - User: config.DataSource{Name: "user", Type: "postgres"}, + Config: config.DataSource{Type: "postgres", Postgres: config.PostgresDataSource{Name: "identity"}}, + Runtime: config.DataSource{Type: "postgres", Postgres: config.PostgresDataSource{Name: "runtime"}}, + User: config.DataSource{Type: "postgres", Postgres: config.PostgresDataSource{Name: "user"}}, }, } err = config.InitializeThunderRuntime(".", dummyConfig) diff --git a/backend/internal/system/database/provider/redisprovider.go b/backend/internal/system/database/provider/redisprovider.go new file mode 100644 index 000000000..d0f93bf31 --- /dev/null +++ b/backend/internal/system/database/provider/redisprovider.go @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package provider + +import ( + "context" + "sync" + + "github.com/redis/go-redis/v9" + + "github.com/asgardeo/thunder/internal/system/config" + "github.com/asgardeo/thunder/internal/system/log" +) + +// DataSourceTypeRedis is the type identifier for a Redis data source. +const DataSourceTypeRedis = "redis" + +// RedisProviderInterface provides a Redis client for runtime store operations. +type RedisProviderInterface interface { + GetRedisClient() *redis.Client + GetKeyPrefix() string +} + +// RedisProviderCloser is a separate interface for closing the provider. +// Only the lifecycle manager should use this interface. +type RedisProviderCloser interface { + Close() error +} + +// redisProvider is the implementation of RedisProviderInterface. +type redisProvider struct { + client *redis.Client + keyPrefix string + mu sync.RWMutex +} + +var ( + redisInstance *redisProvider + redisOnce sync.Once +) + +// initRedisProvider initializes the singleton Redis provider. +func initRedisProvider() { + redisOnce.Do(func() { + cfg := config.GetThunderRuntime().Config.Database.Runtime + // This is a no-op when runtime.type is not "redis". + if cfg.Type != DataSourceTypeRedis { + return + } + + logger := log.GetLogger().With(log.String(log.LoggerKeyComponentName, "RedisProvider")) + + r := cfg.Redis + client := redis.NewClient(&redis.Options{ + Addr: r.Address, + Username: r.Username, + Password: r.Password, + DB: r.DB, + }) + + if err := client.Ping(context.Background()).Err(); err != nil { + if closeErr := client.Close(); closeErr != nil { + logger.Fatal("Failed to connect to Redis runtime store; also failed to close client", + log.Error(err), log.String("closeError", closeErr.Error())) + } + logger.Fatal("Failed to connect to Redis runtime store", log.Error(err)) + } + + logger.Info("Connected to Redis runtime store", log.String("address", r.Address)) + redisInstance = &redisProvider{ + client: client, + keyPrefix: r.KeyPrefix, + } + }) +} + +// GetRedisProvider returns the singleton Redis provider. +func GetRedisProvider() RedisProviderInterface { + initRedisProvider() + return redisInstance +} + +// GetRedisProviderCloser returns the Redis provider with closing capability. +func GetRedisProviderCloser() RedisProviderCloser { + initRedisProvider() + return redisInstance +} + +// GetRedisClient returns the underlying Redis client. +func (r *redisProvider) GetRedisClient() *redis.Client { + r.mu.RLock() + defer r.mu.RUnlock() + return r.client +} + +// GetKeyPrefix returns the key prefix for namespacing Redis keys. +func (r *redisProvider) GetKeyPrefix() string { + return r.keyPrefix +} + +// Close closes the Redis connection. Called by the lifecycle manager on shutdown. +func (r *redisProvider) Close() error { + r.mu.Lock() + defer r.mu.Unlock() + if r.client != nil { + if err := r.client.Close(); err != nil { + return err + } + r.client = nil + } + return nil +} diff --git a/backend/internal/system/database/provider/redisprovider_test.go b/backend/internal/system/database/provider/redisprovider_test.go new file mode 100644 index 000000000..697f08f3e --- /dev/null +++ b/backend/internal/system/database/provider/redisprovider_test.go @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package provider + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +// RedisProviderTestSuite tests the redisProvider struct methods directly. +// +// Note: initRedisProvider / GetRedisProvider / GetRedisProviderCloser rely on +// a package-level sync.Once and require a live Redis connection. Those paths +// are validated by integration tests against a real Redis server. +type RedisProviderTestSuite struct { + suite.Suite +} + +func TestRedisProviderTestSuite(t *testing.T) { + suite.Run(t, new(RedisProviderTestSuite)) +} + +func (suite *RedisProviderTestSuite) TestGetKeyPrefix() { + p := &redisProvider{keyPrefix: "thunder"} + suite.Equal("thunder", p.GetKeyPrefix()) +} + +func (suite *RedisProviderTestSuite) TestGetRedisClient_Nil() { + p := &redisProvider{client: nil} + suite.Nil(p.GetRedisClient()) +} + +func (suite *RedisProviderTestSuite) TestClose_NilClient_NoError() { + // Closing when the client was never initialized should be a no-op. + p := &redisProvider{client: nil} + err := p.Close() + suite.NoError(err) +} diff --git a/backend/internal/system/healthcheck/service/healthcheckservice.go b/backend/internal/system/healthcheck/service/healthcheckservice.go index 7ac701589..4ff6e7ae2 100644 --- a/backend/internal/system/healthcheck/service/healthcheckservice.go +++ b/backend/internal/system/healthcheck/service/healthcheckservice.go @@ -20,8 +20,10 @@ package service import ( + "context" "sync" + "github.com/asgardeo/thunder/internal/system/config" dbmodel "github.com/asgardeo/thunder/internal/system/database/model" "github.com/asgardeo/thunder/internal/system/database/provider" "github.com/asgardeo/thunder/internal/system/healthcheck/model" @@ -40,14 +42,16 @@ type HealthCheckServiceInterface interface { // HealthCheckService is the default implementation of the HealthCheckServiceInterface. type HealthCheckService struct { - DBProvider provider.DBProviderInterface + DBProvider provider.DBProviderInterface + RedisProvider provider.RedisProviderInterface } // GetHealthCheckService returns a singleton instance of HealthCheckService. func GetHealthCheckService() HealthCheckServiceInterface { once.Do(func() { instance = &HealthCheckService{ - DBProvider: provider.GetDBProvider(), + DBProvider: provider.GetDBProvider(), + RedisProvider: provider.GetRedisProvider(), } }) return instance @@ -94,10 +98,27 @@ func (hcs *HealthCheckService) checkConfigDatabaseStatus(query dbmodel.DBQuery) // checkRuntimeDatabaseStatus checks the status of the runtime database with the specified query. func (hcs *HealthCheckService) checkRuntimeDatabaseStatus(query dbmodel.DBQuery) model.Status { + if config.GetThunderRuntime().Config.Database.Runtime.Type == provider.DataSourceTypeRedis { + return hcs.checkRedisRuntimeStatus() + } dbClient, err := hcs.DBProvider.GetRuntimeDBClient() return hcs.executeDatabaseHealthCheck("RuntimeDB", dbClient, err, query) } +// checkRedisRuntimeStatus checks the health of the Redis runtime store via Ping. +func (hcs *HealthCheckService) checkRedisRuntimeStatus() model.Status { + logger := log.GetLogger().With(log.String(log.LoggerKeyComponentName, "HealthCheckService")) + if hcs.RedisProvider == nil { + logger.Error("Redis runtime provider is not initialized") + return model.StatusDown + } + if err := hcs.RedisProvider.GetRedisClient().Ping(context.Background()).Err(); err != nil { + logger.Error("Failed to ping Redis runtime store", log.Error(err)) + return model.StatusDown + } + return model.StatusUp +} + // checkUserDatabaseStatus checks the status of the runtime database with the specified query. func (hcs *HealthCheckService) checkUserDatabaseStatus(query dbmodel.DBQuery) model.Status { dbClient, err := hcs.DBProvider.GetUserDBClient() diff --git a/backend/internal/system/healthcheck/service/healthcheckservice_test.go b/backend/internal/system/healthcheck/service/healthcheckservice_test.go index f91bae179..f237c8eab 100644 --- a/backend/internal/system/healthcheck/service/healthcheckservice_test.go +++ b/backend/internal/system/healthcheck/service/healthcheckservice_test.go @@ -49,16 +49,16 @@ func (suite *HealthCheckServiceTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, User: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } diff --git a/backend/internal/userprovider/init_test.go b/backend/internal/userprovider/init_test.go index aedbeb76c..56cbe3e87 100644 --- a/backend/internal/userprovider/init_test.go +++ b/backend/internal/userprovider/init_test.go @@ -39,12 +39,12 @@ func (suite *InitUserProviderTestSuite) SetupTest() { testConfig := &config.Config{ Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, Runtime: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } diff --git a/backend/internal/userschema/declarative_resource_internal_test.go b/backend/internal/userschema/declarative_resource_internal_test.go index 6cd357edf..4bc4f14e7 100644 --- a/backend/internal/userschema/declarative_resource_internal_test.go +++ b/backend/internal/userschema/declarative_resource_internal_test.go @@ -336,8 +336,8 @@ func TestLoadDeclarativeResources(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -432,8 +432,8 @@ func TestLoadDeclarativeResources_WithNilOUService(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } diff --git a/backend/internal/userschema/init_test.go b/backend/internal/userschema/init_test.go index 46eef8f50..8191a7be2 100644 --- a/backend/internal/userschema/init_test.go +++ b/backend/internal/userschema/init_test.go @@ -78,8 +78,8 @@ func (suite *InitTestSuite) TestInitialize() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -101,8 +101,8 @@ func (suite *InitTestSuite) TestRegisterRoutes_ListEndpoint() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -128,8 +128,8 @@ func (suite *InitTestSuite) TestRegisterRoutes_CreateEndpoint() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -162,8 +162,8 @@ func (suite *InitTestSuite) TestInitialize_DBTransactionerError() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "invalid-db-type", - Path: ":memory:", + Type: "invalid-db-type", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -186,8 +186,8 @@ func (suite *InitTestSuite) TestRegisterRoutes_GetByIDEndpoint() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -213,8 +213,8 @@ func (suite *InitTestSuite) TestRegisterRoutes_UpdateEndpoint() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -240,8 +240,8 @@ func (suite *InitTestSuite) TestRegisterRoutes_DeleteEndpoint() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -267,8 +267,8 @@ func (suite *InitTestSuite) TestRegisterRoutes_CORSPreflight() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -294,8 +294,8 @@ func (suite *InitTestSuite) TestRegisterRoutes_CORSPreflightByID() { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -495,8 +495,8 @@ func TestInitialize_Standalone(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -527,8 +527,8 @@ func TestInitializeStore_MutableMode(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -560,8 +560,8 @@ func TestInitializeStore_DeclarativeMode(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -591,8 +591,8 @@ func TestInitializeStore_CompositeMode(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -624,8 +624,8 @@ func TestInitializeStore_DefaultFallbackToMutable(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -657,8 +657,8 @@ func TestInitializeStore_GlobalDeclarativeEnabled(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -688,8 +688,8 @@ func TestInitialize_MutableMode(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -727,8 +727,8 @@ func TestInitialize_StoreModes(t *testing.T) { UserSchema: config.UserSchemaConfig{Store: m.store}, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, } @@ -1103,8 +1103,8 @@ func TestInitialize_WithDeclarativeResourcesEnabled_InvalidYAML(t *testing.T) { }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Crypto: config.CryptoConfig{ @@ -1163,8 +1163,8 @@ schema: | }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Crypto: config.CryptoConfig{ @@ -1222,8 +1222,8 @@ schema: | }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Crypto: config.CryptoConfig{ @@ -1291,8 +1291,8 @@ schema: | }, Database: config.DatabaseConfig{ Config: config.DataSource{ - Type: "sqlite", - Path: ":memory:", + Type: "sqlite", + SQLite: config.SQLiteDataSource{Path: ":memory:"}, }, }, Crypto: config.CryptoConfig{ diff --git a/backend/tests/mocks/attributecachemock/redisClient_mock.go b/backend/tests/mocks/attributecachemock/redisClient_mock.go new file mode 100644 index 000000000..ec993656a --- /dev/null +++ b/backend/tests/mocks/attributecachemock/redisClient_mock.go @@ -0,0 +1,366 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package attributecachemock + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// newRedisClientMock creates a new instance of redisClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newRedisClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *redisClientMock { + mock := &redisClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// redisClientMock is an autogenerated mock type for the redisClient type +type redisClientMock struct { + mock.Mock +} + +type redisClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *redisClientMock) EXPECT() *redisClientMock_Expecter { + return &redisClientMock_Expecter{mock: &_m.Mock} +} + +// Del provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Del(ctx context.Context, keys ...string) *redis.IntCmd { + // string + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Del") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// redisClientMock_Del_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Del' +type redisClientMock_Del_Call struct { + *mock.Call +} + +// Del is a helper method to define mock.On call +// - ctx context.Context +// - keys ...string +func (_e *redisClientMock_Expecter) Del(ctx interface{}, keys ...interface{}) *redisClientMock_Del_Call { + return &redisClientMock_Del_Call{Call: _e.mock.On("Del", + append([]interface{}{ctx}, keys...)...)} +} + +func (_c *redisClientMock_Del_Call) Run(run func(ctx context.Context, keys ...string)) *redisClientMock_Del_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *redisClientMock_Del_Call) Return(intCmd *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *redisClientMock_Del_Call) RunAndReturn(run func(ctx context.Context, keys ...string) *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(run) + return _c +} + +// Expire provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd { + ret := _mock.Called(ctx, key, expiration) + + if len(ret) == 0 { + panic("no return value specified for Expire") + } + + var r0 *redis.BoolCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Duration) *redis.BoolCmd); ok { + r0 = returnFunc(ctx, key, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.BoolCmd) + } + } + return r0 +} + +// redisClientMock_Expire_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Expire' +type redisClientMock_Expire_Call struct { + *mock.Call +} + +// Expire is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - expiration time.Duration +func (_e *redisClientMock_Expecter) Expire(ctx interface{}, key interface{}, expiration interface{}) *redisClientMock_Expire_Call { + return &redisClientMock_Expire_Call{Call: _e.mock.On("Expire", ctx, key, expiration)} +} + +func (_c *redisClientMock_Expire_Call) Run(run func(ctx context.Context, key string, expiration time.Duration)) *redisClientMock_Expire_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 time.Duration + if args[2] != nil { + arg2 = args[2].(time.Duration) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *redisClientMock_Expire_Call) Return(boolCmd *redis.BoolCmd) *redisClientMock_Expire_Call { + _c.Call.Return(boolCmd) + return _c +} + +func (_c *redisClientMock_Expire_Call) RunAndReturn(run func(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd) *redisClientMock_Expire_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Get(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// redisClientMock_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type redisClientMock_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *redisClientMock_Expecter) Get(ctx interface{}, key interface{}) *redisClientMock_Get_Call { + return &redisClientMock_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *redisClientMock_Get_Call) Run(run func(ctx context.Context, key string)) *redisClientMock_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_Get_Call) Return(stringCmd *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *redisClientMock_Get_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + ret := _mock.Called(ctx, key, value, expiration) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, interface{}, time.Duration) *redis.StatusCmd); ok { + r0 = returnFunc(ctx, key, value, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// redisClientMock_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type redisClientMock_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value interface{} +// - expiration time.Duration +func (_e *redisClientMock_Expecter) Set(ctx interface{}, key interface{}, value interface{}, expiration interface{}) *redisClientMock_Set_Call { + return &redisClientMock_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expiration)} +} + +func (_c *redisClientMock_Set_Call) Run(run func(ctx context.Context, key string, value interface{}, expiration time.Duration)) *redisClientMock_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 interface{} + if args[2] != nil { + arg2 = args[2].(interface{}) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *redisClientMock_Set_Call) Return(statusCmd *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *redisClientMock_Set_Call) RunAndReturn(run func(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(run) + return _c +} + +// TTL provides a mock function for the type redisClientMock +func (_mock *redisClientMock) TTL(ctx context.Context, key string) *redis.DurationCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for TTL") + } + + var r0 *redis.DurationCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.DurationCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.DurationCmd) + } + } + return r0 +} + +// redisClientMock_TTL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TTL' +type redisClientMock_TTL_Call struct { + *mock.Call +} + +// TTL is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *redisClientMock_Expecter) TTL(ctx interface{}, key interface{}) *redisClientMock_TTL_Call { + return &redisClientMock_TTL_Call{Call: _e.mock.On("TTL", ctx, key)} +} + +func (_c *redisClientMock_TTL_Call) Run(run func(ctx context.Context, key string)) *redisClientMock_TTL_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_TTL_Call) Return(durationCmd *redis.DurationCmd) *redisClientMock_TTL_Call { + _c.Call.Return(durationCmd) + return _c +} + +func (_c *redisClientMock_TTL_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.DurationCmd) *redisClientMock_TTL_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/tests/mocks/database/providermock/RedisProviderCloser_mock.go b/backend/tests/mocks/database/providermock/RedisProviderCloser_mock.go new file mode 100644 index 000000000..4e93b24d1 --- /dev/null +++ b/backend/tests/mocks/database/providermock/RedisProviderCloser_mock.go @@ -0,0 +1,80 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package providermock + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewRedisProviderCloserMock creates a new instance of RedisProviderCloserMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRedisProviderCloserMock(t interface { + mock.TestingT + Cleanup(func()) +}) *RedisProviderCloserMock { + mock := &RedisProviderCloserMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// RedisProviderCloserMock is an autogenerated mock type for the RedisProviderCloser type +type RedisProviderCloserMock struct { + mock.Mock +} + +type RedisProviderCloserMock_Expecter struct { + mock *mock.Mock +} + +func (_m *RedisProviderCloserMock) EXPECT() *RedisProviderCloserMock_Expecter { + return &RedisProviderCloserMock_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function for the type RedisProviderCloserMock +func (_mock *RedisProviderCloserMock) Close() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// RedisProviderCloserMock_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type RedisProviderCloserMock_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *RedisProviderCloserMock_Expecter) Close() *RedisProviderCloserMock_Close_Call { + return &RedisProviderCloserMock_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *RedisProviderCloserMock_Close_Call) Run(run func()) *RedisProviderCloserMock_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *RedisProviderCloserMock_Close_Call) Return(err error) *RedisProviderCloserMock_Close_Call { + _c.Call.Return(err) + return _c +} + +func (_c *RedisProviderCloserMock_Close_Call) RunAndReturn(run func() error) *RedisProviderCloserMock_Close_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/tests/mocks/database/providermock/RedisProviderInterface_mock.go b/backend/tests/mocks/database/providermock/RedisProviderInterface_mock.go new file mode 100644 index 000000000..f40813aaa --- /dev/null +++ b/backend/tests/mocks/database/providermock/RedisProviderInterface_mock.go @@ -0,0 +1,127 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package providermock + +import ( + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// NewRedisProviderInterfaceMock creates a new instance of RedisProviderInterfaceMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRedisProviderInterfaceMock(t interface { + mock.TestingT + Cleanup(func()) +}) *RedisProviderInterfaceMock { + mock := &RedisProviderInterfaceMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// RedisProviderInterfaceMock is an autogenerated mock type for the RedisProviderInterface type +type RedisProviderInterfaceMock struct { + mock.Mock +} + +type RedisProviderInterfaceMock_Expecter struct { + mock *mock.Mock +} + +func (_m *RedisProviderInterfaceMock) EXPECT() *RedisProviderInterfaceMock_Expecter { + return &RedisProviderInterfaceMock_Expecter{mock: &_m.Mock} +} + +// GetKeyPrefix provides a mock function for the type RedisProviderInterfaceMock +func (_mock *RedisProviderInterfaceMock) GetKeyPrefix() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetKeyPrefix") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// RedisProviderInterfaceMock_GetKeyPrefix_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetKeyPrefix' +type RedisProviderInterfaceMock_GetKeyPrefix_Call struct { + *mock.Call +} + +// GetKeyPrefix is a helper method to define mock.On call +func (_e *RedisProviderInterfaceMock_Expecter) GetKeyPrefix() *RedisProviderInterfaceMock_GetKeyPrefix_Call { + return &RedisProviderInterfaceMock_GetKeyPrefix_Call{Call: _e.mock.On("GetKeyPrefix")} +} + +func (_c *RedisProviderInterfaceMock_GetKeyPrefix_Call) Run(run func()) *RedisProviderInterfaceMock_GetKeyPrefix_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *RedisProviderInterfaceMock_GetKeyPrefix_Call) Return(s string) *RedisProviderInterfaceMock_GetKeyPrefix_Call { + _c.Call.Return(s) + return _c +} + +func (_c *RedisProviderInterfaceMock_GetKeyPrefix_Call) RunAndReturn(run func() string) *RedisProviderInterfaceMock_GetKeyPrefix_Call { + _c.Call.Return(run) + return _c +} + +// GetRedisClient provides a mock function for the type RedisProviderInterfaceMock +func (_mock *RedisProviderInterfaceMock) GetRedisClient() *redis.Client { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetRedisClient") + } + + var r0 *redis.Client + if returnFunc, ok := ret.Get(0).(func() *redis.Client); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Client) + } + } + return r0 +} + +// RedisProviderInterfaceMock_GetRedisClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRedisClient' +type RedisProviderInterfaceMock_GetRedisClient_Call struct { + *mock.Call +} + +// GetRedisClient is a helper method to define mock.On call +func (_e *RedisProviderInterfaceMock_Expecter) GetRedisClient() *RedisProviderInterfaceMock_GetRedisClient_Call { + return &RedisProviderInterfaceMock_GetRedisClient_Call{Call: _e.mock.On("GetRedisClient")} +} + +func (_c *RedisProviderInterfaceMock_GetRedisClient_Call) Run(run func()) *RedisProviderInterfaceMock_GetRedisClient_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *RedisProviderInterfaceMock_GetRedisClient_Call) Return(client *redis.Client) *RedisProviderInterfaceMock_GetRedisClient_Call { + _c.Call.Return(client) + return _c +} + +func (_c *RedisProviderInterfaceMock_GetRedisClient_Call) RunAndReturn(run func() *redis.Client) *RedisProviderInterfaceMock_GetRedisClient_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/tests/mocks/flow/flowexecmock/redisClient_mock.go b/backend/tests/mocks/flow/flowexecmock/redisClient_mock.go new file mode 100644 index 000000000..dcfc0ab32 --- /dev/null +++ b/backend/tests/mocks/flow/flowexecmock/redisClient_mock.go @@ -0,0 +1,689 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package flowexecmock + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// newRedisClientMock creates a new instance of redisClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newRedisClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *redisClientMock { + mock := &redisClientMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// redisClientMock is an autogenerated mock type for the redisClient type +type redisClientMock struct { + mock.Mock +} + +type redisClientMock_Expecter struct { + mock *mock.Mock +} + +func (_m *redisClientMock) EXPECT() *redisClientMock_Expecter { + return &redisClientMock_Expecter{mock: &_m.Mock} +} + +// Del provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Del(ctx context.Context, keys ...string) *redis.IntCmd { + // string + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Del") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// redisClientMock_Del_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Del' +type redisClientMock_Del_Call struct { + *mock.Call +} + +// Del is a helper method to define mock.On call +// - ctx context.Context +// - keys ...string +func (_e *redisClientMock_Expecter) Del(ctx interface{}, keys ...interface{}) *redisClientMock_Del_Call { + return &redisClientMock_Del_Call{Call: _e.mock.On("Del", + append([]interface{}{ctx}, keys...)...)} +} + +func (_c *redisClientMock_Del_Call) Run(run func(ctx context.Context, keys ...string)) *redisClientMock_Del_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *redisClientMock_Del_Call) Return(intCmd *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *redisClientMock_Del_Call) RunAndReturn(run func(ctx context.Context, keys ...string) *redis.IntCmd) *redisClientMock_Del_Call { + _c.Call.Return(run) + return _c +} + +// Eval provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, script, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Eval") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, script, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// redisClientMock_Eval_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Eval' +type redisClientMock_Eval_Call struct { + *mock.Call +} + +// Eval is a helper method to define mock.On call +// - ctx context.Context +// - script string +// - keys []string +// - args ...interface{} +func (_e *redisClientMock_Expecter) Eval(ctx interface{}, script interface{}, keys interface{}, args ...interface{}) *redisClientMock_Eval_Call { + return &redisClientMock_Eval_Call{Call: _e.mock.On("Eval", + append([]interface{}{ctx, script, keys}, args...)...)} +} + +func (_c *redisClientMock_Eval_Call) Run(run func(ctx context.Context, script string, keys []string, args ...interface{})) *redisClientMock_Eval_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *redisClientMock_Eval_Call) Return(cmd *redis.Cmd) *redisClientMock_Eval_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *redisClientMock_Eval_Call) RunAndReturn(run func(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd) *redisClientMock_Eval_Call { + _c.Call.Return(run) + return _c +} + +// EvalRO provides a mock function for the type redisClientMock +func (_mock *redisClientMock) EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, script, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalRO") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, script, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// redisClientMock_EvalRO_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalRO' +type redisClientMock_EvalRO_Call struct { + *mock.Call +} + +// EvalRO is a helper method to define mock.On call +// - ctx context.Context +// - script string +// - keys []string +// - args ...interface{} +func (_e *redisClientMock_Expecter) EvalRO(ctx interface{}, script interface{}, keys interface{}, args ...interface{}) *redisClientMock_EvalRO_Call { + return &redisClientMock_EvalRO_Call{Call: _e.mock.On("EvalRO", + append([]interface{}{ctx, script, keys}, args...)...)} +} + +func (_c *redisClientMock_EvalRO_Call) Run(run func(ctx context.Context, script string, keys []string, args ...interface{})) *redisClientMock_EvalRO_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *redisClientMock_EvalRO_Call) Return(cmd *redis.Cmd) *redisClientMock_EvalRO_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *redisClientMock_EvalRO_Call) RunAndReturn(run func(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd) *redisClientMock_EvalRO_Call { + _c.Call.Return(run) + return _c +} + +// EvalSha provides a mock function for the type redisClientMock +func (_mock *redisClientMock) EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, sha1, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalSha") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, sha1, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// redisClientMock_EvalSha_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalSha' +type redisClientMock_EvalSha_Call struct { + *mock.Call +} + +// EvalSha is a helper method to define mock.On call +// - ctx context.Context +// - sha1 string +// - keys []string +// - args ...interface{} +func (_e *redisClientMock_Expecter) EvalSha(ctx interface{}, sha1 interface{}, keys interface{}, args ...interface{}) *redisClientMock_EvalSha_Call { + return &redisClientMock_EvalSha_Call{Call: _e.mock.On("EvalSha", + append([]interface{}{ctx, sha1, keys}, args...)...)} +} + +func (_c *redisClientMock_EvalSha_Call) Run(run func(ctx context.Context, sha1 string, keys []string, args ...interface{})) *redisClientMock_EvalSha_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *redisClientMock_EvalSha_Call) Return(cmd *redis.Cmd) *redisClientMock_EvalSha_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *redisClientMock_EvalSha_Call) RunAndReturn(run func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd) *redisClientMock_EvalSha_Call { + _c.Call.Return(run) + return _c +} + +// EvalShaRO provides a mock function for the type redisClientMock +func (_mock *redisClientMock) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd { + var _ca []interface{} + _ca = append(_ca, ctx, sha1, keys) + _ca = append(_ca, args...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for EvalShaRO") + } + + var r0 *redis.Cmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, ...interface{}) *redis.Cmd); ok { + r0 = returnFunc(ctx, sha1, keys, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.Cmd) + } + } + return r0 +} + +// redisClientMock_EvalShaRO_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EvalShaRO' +type redisClientMock_EvalShaRO_Call struct { + *mock.Call +} + +// EvalShaRO is a helper method to define mock.On call +// - ctx context.Context +// - sha1 string +// - keys []string +// - args ...interface{} +func (_e *redisClientMock_Expecter) EvalShaRO(ctx interface{}, sha1 interface{}, keys interface{}, args ...interface{}) *redisClientMock_EvalShaRO_Call { + return &redisClientMock_EvalShaRO_Call{Call: _e.mock.On("EvalShaRO", + append([]interface{}{ctx, sha1, keys}, args...)...)} +} + +func (_c *redisClientMock_EvalShaRO_Call) Run(run func(ctx context.Context, sha1 string, keys []string, args ...interface{})) *redisClientMock_EvalShaRO_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 []interface{} + variadicArgs := make([]interface{}, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *redisClientMock_EvalShaRO_Call) Return(cmd *redis.Cmd) *redisClientMock_EvalShaRO_Call { + _c.Call.Return(cmd) + return _c +} + +func (_c *redisClientMock_EvalShaRO_Call) RunAndReturn(run func(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd) *redisClientMock_EvalShaRO_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Get(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// redisClientMock_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type redisClientMock_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *redisClientMock_Expecter) Get(ctx interface{}, key interface{}) *redisClientMock_Get_Call { + return &redisClientMock_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *redisClientMock_Get_Call) Run(run func(ctx context.Context, key string)) *redisClientMock_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_Get_Call) Return(stringCmd *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *redisClientMock_Get_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *redisClientMock_Get_Call { + _c.Call.Return(run) + return _c +} + +// ScriptExists provides a mock function for the type redisClientMock +func (_mock *redisClientMock) ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd { + // string + _va := make([]interface{}, len(hashes)) + for _i := range hashes { + _va[_i] = hashes[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for ScriptExists") + } + + var r0 *redis.BoolSliceCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.BoolSliceCmd); ok { + r0 = returnFunc(ctx, hashes...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.BoolSliceCmd) + } + } + return r0 +} + +// redisClientMock_ScriptExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScriptExists' +type redisClientMock_ScriptExists_Call struct { + *mock.Call +} + +// ScriptExists is a helper method to define mock.On call +// - ctx context.Context +// - hashes ...string +func (_e *redisClientMock_Expecter) ScriptExists(ctx interface{}, hashes ...interface{}) *redisClientMock_ScriptExists_Call { + return &redisClientMock_ScriptExists_Call{Call: _e.mock.On("ScriptExists", + append([]interface{}{ctx}, hashes...)...)} +} + +func (_c *redisClientMock_ScriptExists_Call) Run(run func(ctx context.Context, hashes ...string)) *redisClientMock_ScriptExists_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *redisClientMock_ScriptExists_Call) Return(boolSliceCmd *redis.BoolSliceCmd) *redisClientMock_ScriptExists_Call { + _c.Call.Return(boolSliceCmd) + return _c +} + +func (_c *redisClientMock_ScriptExists_Call) RunAndReturn(run func(ctx context.Context, hashes ...string) *redis.BoolSliceCmd) *redisClientMock_ScriptExists_Call { + _c.Call.Return(run) + return _c +} + +// ScriptLoad provides a mock function for the type redisClientMock +func (_mock *redisClientMock) ScriptLoad(ctx context.Context, script string) *redis.StringCmd { + ret := _mock.Called(ctx, script) + + if len(ret) == 0 { + panic("no return value specified for ScriptLoad") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, script) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// redisClientMock_ScriptLoad_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScriptLoad' +type redisClientMock_ScriptLoad_Call struct { + *mock.Call +} + +// ScriptLoad is a helper method to define mock.On call +// - ctx context.Context +// - script string +func (_e *redisClientMock_Expecter) ScriptLoad(ctx interface{}, script interface{}) *redisClientMock_ScriptLoad_Call { + return &redisClientMock_ScriptLoad_Call{Call: _e.mock.On("ScriptLoad", ctx, script)} +} + +func (_c *redisClientMock_ScriptLoad_Call) Run(run func(ctx context.Context, script string)) *redisClientMock_ScriptLoad_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *redisClientMock_ScriptLoad_Call) Return(stringCmd *redis.StringCmd) *redisClientMock_ScriptLoad_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *redisClientMock_ScriptLoad_Call) RunAndReturn(run func(ctx context.Context, script string) *redis.StringCmd) *redisClientMock_ScriptLoad_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type redisClientMock +func (_mock *redisClientMock) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + ret := _mock.Called(ctx, key, value, expiration) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, interface{}, time.Duration) *redis.StatusCmd); ok { + r0 = returnFunc(ctx, key, value, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// redisClientMock_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type redisClientMock_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value interface{} +// - expiration time.Duration +func (_e *redisClientMock_Expecter) Set(ctx interface{}, key interface{}, value interface{}, expiration interface{}) *redisClientMock_Set_Call { + return &redisClientMock_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expiration)} +} + +func (_c *redisClientMock_Set_Call) Run(run func(ctx context.Context, key string, value interface{}, expiration time.Duration)) *redisClientMock_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 interface{} + if args[2] != nil { + arg2 = args[2].(interface{}) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *redisClientMock_Set_Call) Return(statusCmd *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *redisClientMock_Set_Call) RunAndReturn(run func(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd) *redisClientMock_Set_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/tests/resources/deployment.yaml b/backend/tests/resources/deployment.yaml index beedaa7d0..c21c3ddf0 100644 --- a/backend/tests/resources/deployment.yaml +++ b/backend/tests/resources/deployment.yaml @@ -9,25 +9,19 @@ tls: database: config: type: postgres - hostname: localhost - port: 5432 - name: configdb - username: postgres - password: postgres - sslmode: disable - path: "" - options: "" + postgres: + hostname: localhost + port: 5432 + name: configdb + username: postgres + password: postgres + sslmode: disable runtime: type: sqlite - hostname: "" - port: 0 - name: "" - username: "" - password: "" - sslmode: "" - path: "/data/runtime.db" - options: "cache=shared" + sqlite: + path: "/data/runtime.db" + options: "cache=shared" oauth: jwt: diff --git a/docs/content/guides/getting-started/configuration.mdx b/docs/content/guides/getting-started/configuration.mdx index 49e143ceb..b56948750 100644 --- a/docs/content/guides/getting-started/configuration.mdx +++ b/docs/content/guides/getting-started/configuration.mdx @@ -62,6 +62,8 @@ Thunder ships with a self-signed certificate for local development at `repositor Thunder uses three separate databases for different purposes. Each database can be configured independently. +Connection parameters are grouped under a type-specific sub-key (`postgres`, `sqlite`, or `redis`). Only `type` is a top-level field; all other settings belong under the matching sub-key. + ### Config Database Stores identity provider configurations, applications, and authentication flows. @@ -69,36 +71,81 @@ Stores identity provider configurations, applications, and authentication flows. | Setting | Default | Description | |---------|---------|-------------| | `database.config.type` | `sqlite` | Database type (`sqlite` or `postgres`) | -| `database.config.hostname` | `""` | Database server hostname (for PostgreSQL) | -| `database.config.port` | `0` | Database server port (for PostgreSQL) | -| `database.config.name` | `""` | Database name (for PostgreSQL) | -| `database.config.username` | `""` | Database username (for PostgreSQL) | -| `database.config.password` | `""` | Database password (for PostgreSQL) | -| `database.config.sslmode` | `""` | SSL mode for PostgreSQL (`disable`, `require`, `verify-ca`, `verify-full`) | -| `database.config.path` | `repository/database/configdb.db` | SQLite database file path | -| `database.config.options` | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | Database-specific connection options | -| `database.config.max_open_conns` | `500` | Maximum number of open connections | -| `database.config.max_idle_conns` | `100` | Maximum number of idle connections | -| `database.config.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | + +**`database.config.postgres.*`** — only read when `database.config.type: postgres`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `database.config.postgres.hostname` | `""` | Database server hostname | +| `database.config.postgres.port` | `0` | Database server port | +| `database.config.postgres.name` | `""` | Database name | +| `database.config.postgres.username` | `""` | Database username | +| `database.config.postgres.password` | `""` | Database password | +| `database.config.postgres.sslmode` | `""` | SSL mode (`disable`, `require`, `verify-ca`, `verify-full`) | +| `database.config.postgres.max_open_conns` | `500` | Maximum number of open connections | +| `database.config.postgres.max_idle_conns` | `100` | Maximum number of idle connections | +| `database.config.postgres.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | + +**`database.config.sqlite.*`** — only read when `database.config.type: sqlite`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `database.config.sqlite.path` | `repository/database/configdb.db` | SQLite database file path | +| `database.config.sqlite.options` | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | SQLite connection options | +| `database.config.sqlite.max_open_conns` | `500` | Maximum number of open connections | +| `database.config.sqlite.max_idle_conns` | `100` | Maximum number of idle connections | +| `database.config.sqlite.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | ### Runtime Database -Stores runtime data like sessions, tokens, and temporary data. +Stores runtime data like sessions, tokens, and temporary data. The runtime database supports three backend types: `sqlite`, `postgres`, and `redis`. + +| Setting | Default | Description | +|---------|---------|-------------| +| `database.runtime.type` | `sqlite` | Database type (`sqlite`, `postgres`, or `redis`) | + +**`database.runtime.postgres.*`** — only read when `database.runtime.type: postgres`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `database.runtime.postgres.hostname` | `""` | Database server hostname | +| `database.runtime.postgres.port` | `0` | Database server port | +| `database.runtime.postgres.name` | `""` | Database name | +| `database.runtime.postgres.username` | `""` | Database username | +| `database.runtime.postgres.password` | `""` | Database password | +| `database.runtime.postgres.sslmode` | `""` | SSL mode (`disable`, `require`, `verify-ca`, `verify-full`) | +| `database.runtime.postgres.max_open_conns` | `500` | Maximum number of open connections | +| `database.runtime.postgres.max_idle_conns` | `100` | Maximum number of idle connections | +| `database.runtime.postgres.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | + +**`database.runtime.sqlite.*`** — only read when `database.runtime.type: sqlite`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `database.runtime.sqlite.path` | `repository/database/runtimedb.db` | SQLite database file path | +| `database.runtime.sqlite.options` | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | SQLite connection options | +| `database.runtime.sqlite.max_open_conns` | `500` | Maximum number of open connections | +| `database.runtime.sqlite.max_idle_conns` | `100` | Maximum number of idle connections | +| `database.runtime.sqlite.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | + +**`database.runtime.redis.*`** — only read when `database.runtime.type: redis`: | Setting | Default | Description | |---------|---------|-------------| -| `database.runtime.type` | `sqlite` | Database type (`sqlite` or `postgres`) | -| `database.runtime.hostname` | `""` | Database server hostname (for PostgreSQL) | -| `database.runtime.port` | `0` | Database server port (for PostgreSQL) | -| `database.runtime.name` | `""` | Database name (for PostgreSQL) | -| `database.runtime.username` | `""` | Database username (for PostgreSQL) | -| `database.runtime.password` | `""` | Database password (for PostgreSQL) | -| `database.runtime.sslmode` | `""` | SSL mode for PostgreSQL | -| `database.runtime.path` | `repository/database/runtimedb.db` | SQLite database file path | -| `database.runtime.options` | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | Database-specific connection options | -| `database.runtime.max_open_conns` | `500` | Maximum number of open connections | -| `database.runtime.max_idle_conns` | `100` | Maximum number of idle connections | -| `database.runtime.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | +| `database.runtime.redis.address` | `""` | Redis server address in `host:port` format, for example `localhost:6379` | +| `database.runtime.redis.username` | `""` | Redis ACL username — leave empty if ACLs are not configured | +| `database.runtime.redis.password` | `""` | Redis password or ACL user password | +| `database.runtime.redis.db` | `0` | Redis logical database index (0–15) | +| `database.runtime.redis.key_prefix` | `""` | Prefix applied to all Redis keys written by Thunder, for example `thunder:` | + +#### Redis Connection Requirements + +- Thunder requires Redis 6.0 or later. +- Thunder connects to a single Redis node. Cluster mode and Sentinel mode are not currently supported. +- The default Redis port is `6379`. Ensure the port is reachable from the Thunder server. +- Thunder does not enforce TLS at the client configuration level. To encrypt traffic, place a TLS-terminating proxy in front of Redis and point `database.runtime.redis.address` at the proxy endpoint. +- If your Redis deployment uses Access Control Lists (ACLs), create a dedicated user and grant the following commands: `GET`, `SET`, `DEL`, `EXPIRE`, `EVAL`, `EVALSHA`. Set `database.runtime.redis.username` and `database.runtime.redis.password` accordingly. +- Thunder calls `PING` at startup to verify connectivity. The process terminates if the Redis server is unreachable. ### User Database @@ -107,17 +154,30 @@ Stores user profiles and credentials. | Setting | Default | Description | |---------|---------|-------------| | `database.user.type` | `sqlite` | Database type (`sqlite` or `postgres`) | -| `database.user.hostname` | `""` | Database server hostname (for PostgreSQL) | -| `database.user.port` | `0` | Database server port (for PostgreSQL) | -| `database.user.name` | `""` | Database name (for PostgreSQL) | -| `database.user.username` | `""` | Database username (for PostgreSQL) | -| `database.user.password` | `""` | Database password (for PostgreSQL) | -| `database.user.sslmode` | `""` | SSL mode for PostgreSQL | -| `database.user.path` | `repository/database/userdb.db` | SQLite database file path | -| `database.user.options` | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | Database-specific connection options | -| `database.user.max_open_conns` | `500` | Maximum number of open connections | -| `database.user.max_idle_conns` | `100` | Maximum number of idle connections | -| `database.user.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | + +**`database.user.postgres.*`** — only read when `database.user.type: postgres`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `database.user.postgres.hostname` | `""` | Database server hostname | +| `database.user.postgres.port` | `0` | Database server port | +| `database.user.postgres.name` | `""` | Database name | +| `database.user.postgres.username` | `""` | Database username | +| `database.user.postgres.password` | `""` | Database password | +| `database.user.postgres.sslmode` | `""` | SSL mode (`disable`, `require`, `verify-ca`, `verify-full`) | +| `database.user.postgres.max_open_conns` | `500` | Maximum number of open connections | +| `database.user.postgres.max_idle_conns` | `100` | Maximum number of idle connections | +| `database.user.postgres.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | + +**`database.user.sqlite.*`** — only read when `database.user.type: sqlite`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `database.user.sqlite.path` | `repository/database/userdb.db` | SQLite database file path | +| `database.user.sqlite.options` | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | SQLite connection options | +| `database.user.sqlite.max_open_conns` | `500` | Maximum number of open connections | +| `database.user.sqlite.max_idle_conns` | `100` | Maximum number of idle connections | +| `database.user.sqlite.conn_max_lifetime` | `3600` | Maximum connection lifetime in seconds | ## Cache Configuration diff --git a/install/helm/README.md b/install/helm/README.md index 41e44cda1..49edd9123 100644 --- a/install/helm/README.md +++ b/install/helm/README.md @@ -279,24 +279,30 @@ Provide passwords directly in the `password` field. Helm automatically creates a configuration: database: config: - password: "my-secret-password-1" # Auto-converted to Secret! + postgres: + password: "my-secret-password-1" # Auto-converted to Secret! runtime: - password: "my-secret-password-2" + postgres: + password: "my-secret-password-2" + redis: + password: "my-runtime-redis-password" user: - password: "my-secret-password-3" + postgres: + password: "my-secret-password-3" ``` **Best Practice:** Use `--set` flags to avoid committing passwords: ```bash helm install my-thunder oci://ghcr.io/asgardeo/helm-charts/thunder \ - --set configuration.database.config.password=mypass1 \ - --set configuration.database.runtime.password=mypass2 \ - --set configuration.database.user.password=mypass3 + --set configuration.database.config.postgres.password=mypass1 \ + --set configuration.database.runtime.postgres.password=mypass2 \ + --set configuration.database.runtime.redis.password=myredispass \ + --set configuration.database.user.postgres.password=mypass3 ``` Helm automatically: - Creates `-db-credentials` Secret as a pre-install/pre-upgrade hook -- Injects environment variables (`DB_CONFIG_PASSWORD`, `DB_RUNTIME_PASSWORD`, `DB_USER_PASSWORD`) into pods +- Injects environment variables (`DB_CONFIG_PASSWORD`, `DB_RUNTIME_PASSWORD`, `DB_RUNTIME_REDIS_PASSWORD`, `DB_USER_PASSWORD`) into pods - Updates pods when passwords change (via checksum annotations) #### Pattern 2: External Secret (For Production - Recommended) @@ -316,17 +322,24 @@ kubectl create secret generic my-db-secrets \ configuration: database: config: - passwordRef: - name: "my-db-secrets" # Your Secret name - key: "config-password" # Key within Secret + postgres: + passwordRef: + name: "my-db-secrets" # Your Secret name + key: "config-password" # Key within Secret runtime: - passwordRef: - name: "my-db-secrets" - key: "runtime-password" + postgres: + passwordRef: + name: "my-db-secrets" + key: "runtime-password" + redis: + passwordRef: + name: "my-db-secrets" + key: "runtime-redis-password" user: - passwordRef: - name: "my-db-secrets" - key: "user-password" + postgres: + passwordRef: + name: "my-db-secrets" + key: "user-password" ``` When `passwordRef.key` is set, the `password` field is ignored and Helm uses your external Secret. @@ -341,7 +354,7 @@ Thunder reads configuration directly via its application config loader (e.g., fr value is first converted into a Kubernetes Secret by this chart. #### Password Field Options -Each database section (`config`, `runtime`, `user`) supports these fields: +Password fields are available in `configuration.database.config.postgres`, `configuration.database.runtime.postgres`, `configuration.database.runtime.redis`, and `configuration.database.user.postgres`: | Field | Description | Example | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | @@ -381,47 +394,63 @@ Each database section (`config`, `runtime`, `user`) supports these fields: | `configuration.crypto.keys[].certFile` | Signing certificate file path | `repository/resources/security/signing.cert` | | `configuration.crypto.keys[].keyFile` | Signing key file path | `repository/resources/security/signing.key` | | `configuration.database.config.type` | Config database type (postgres or sqlite) | `postgres` | -| `configuration.database.config.sqlitePath` | SQLite database path (for SQLite only) | `repository/database/configdb.db` | -| `configuration.database.config.sqliteOptions` | SQLite options (for SQLite only) | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | -| `configuration.database.config.name` | Postgres database name (for postgres only) | `configdb` | -| `configuration.database.config.host` | Postgres host (for postgres only) | `localhost` | -| `configuration.database.config.port` | Postgres port (for postgres only) | `5432` | -| `configuration.database.config.username` | Postgres username (for postgres only) | `asgthunder` | -| `configuration.database.config.password` | Database password - supports plaintext. When `passwordRef.key` is set, this field is ignored and the external Secret is used instead. | `asgthunder` | -| `configuration.database.config.passwordRef.name` | Kubernetes Secret name for config database password. Leave empty to use auto-created `-db-credentials` Secret when password field is set | `""` | -| `configuration.database.config.passwordRef.key` | Kubernetes Secret key for config database password. When set, overrides `password` field and uses external Secret | `""` | -| `configuration.database.config.sslmode` | Postgres SSL mode (for postgres only) | `require` | -| `configuration.database.config.max_open_conns` | Maximum number of open connections to the database | `500` | -| `configuration.database.config.max_idle_conns` | Maximum number of idle connections in the pool | `100` | -| `configuration.database.config.conn_max_lifetime` | Maximum lifetime of a connection in seconds | `3600` | -| `configuration.database.runtime.type` | Runtime database type (postgres or sqlite) | `postgres` | -| `configuration.database.runtime.sqlitePath` | SQLite database path (for SQLite only) | `repository/database/runtimedb.db` | -| `configuration.database.runtime.sqliteOptions` | SQLite options (for SQLite only) | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | -| `configuration.database.runtime.name` | Postgres database name (for postgres only) | `runtimedb` | -| `configuration.database.runtime.host` | Postgres host (for postgres only) | `localhost` | -| `configuration.database.runtime.port` | Postgres port (for postgres only) | `5432` | -| `configuration.database.runtime.username` | Postgres username (for postgres only) | `asgthunder` | -| `configuration.database.runtime.password` | Database password - supports plaintext. When `passwordRef.key` is set, this field is ignored and the external Secret is used instead. | `asgthunder` | -| `configuration.database.runtime.passwordRef.name` | Kubernetes Secret name for runtime database password. Leave empty to use auto-created `-db-credentials` Secret when password field is set | `""` | -| `configuration.database.runtime.passwordRef.key` | Kubernetes Secret key for runtime database password. When set, overrides `password` field and uses external Secret | `""` | -| `configuration.database.runtime.sslmode` | Postgres SSL mode (for postgres only) | `require` | -| `configuration.database.runtime.max_open_conns` | Maximum number of open connections to the database | `500` | -| `configuration.database.runtime.max_idle_conns` | Maximum number of idle connections in the pool | `100` | -| `configuration.database.runtime.conn_max_lifetime` | Maximum lifetime of a connection in seconds | `3600` | +| `configuration.database.config.sqlite.path` | SQLite database path (for SQLite only) | `repository/database/configdb.db` | +| `configuration.database.config.sqlite.options` | SQLite options (for SQLite only) | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | +| `configuration.database.config.sqlite.max_open_conns` | Maximum number of open connections for SQLite | `500` | +| `configuration.database.config.sqlite.max_idle_conns` | Maximum number of idle SQLite connections | `100` | +| `configuration.database.config.sqlite.conn_max_lifetime` | Maximum SQLite connection lifetime in seconds | `3600` | +| `configuration.database.config.postgres.name` | Postgres database name (for postgres only) | `configdb` | +| `configuration.database.config.postgres.hostname` | Postgres hostname (for postgres only) | `localhost` | +| `configuration.database.config.postgres.port` | Postgres port (for postgres only) | `5432` | +| `configuration.database.config.postgres.username` | Postgres username (for postgres only) | `asgthunder` | +| `configuration.database.config.postgres.password` | Config Postgres password - supports plaintext. When `passwordRef.key` is set, this field is ignored and the external Secret is used instead. | `asgthunder` | +| `configuration.database.config.postgres.passwordRef.name` | Kubernetes Secret name for config Postgres password | `""` | +| `configuration.database.config.postgres.passwordRef.key` | Kubernetes Secret key for config Postgres password | `""` | +| `configuration.database.config.postgres.sslmode` | Postgres SSL mode (for postgres only) | `require` | +| `configuration.database.config.postgres.max_open_conns` | Maximum number of open connections to the database | `500` | +| `configuration.database.config.postgres.max_idle_conns` | Maximum number of idle connections in the pool | `100` | +| `configuration.database.config.postgres.conn_max_lifetime` | Maximum lifetime of a connection in seconds | `3600` | +| `configuration.database.runtime.type` | Runtime database type (`postgres`, `sqlite`, or `redis`) | `postgres` | +| `configuration.database.runtime.sqlite.path` | SQLite database path (for SQLite only) | `repository/database/runtimedb.db` | +| `configuration.database.runtime.sqlite.options` | SQLite options (for SQLite only) | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | +| `configuration.database.runtime.sqlite.max_open_conns` | Maximum number of open connections for SQLite | `500` | +| `configuration.database.runtime.sqlite.max_idle_conns` | Maximum number of idle SQLite connections | `100` | +| `configuration.database.runtime.sqlite.conn_max_lifetime` | Maximum SQLite connection lifetime in seconds | `3600` | +| `configuration.database.runtime.postgres.name` | Postgres database name (for postgres only) | `runtimedb` | +| `configuration.database.runtime.postgres.hostname` | Postgres hostname (for postgres only) | `localhost` | +| `configuration.database.runtime.postgres.port` | Postgres port (for postgres only) | `5432` | +| `configuration.database.runtime.postgres.username` | Postgres username (for postgres only) | `asgthunder` | +| `configuration.database.runtime.postgres.password` | Runtime Postgres password - supports plaintext. When `passwordRef.key` is set, this field is ignored and the external Secret is used instead. | `asgthunder` | +| `configuration.database.runtime.postgres.passwordRef.name` | Kubernetes Secret name for runtime Postgres password | `""` | +| `configuration.database.runtime.postgres.passwordRef.key` | Kubernetes Secret key for runtime Postgres password | `""` | +| `configuration.database.runtime.postgres.sslmode` | Postgres SSL mode (for postgres only) | `require` | +| `configuration.database.runtime.postgres.max_open_conns` | Maximum number of open connections to the database | `500` | +| `configuration.database.runtime.postgres.max_idle_conns` | Maximum number of idle connections in the pool | `100` | +| `configuration.database.runtime.postgres.conn_max_lifetime` | Maximum lifetime of a connection in seconds | `3600` | +| `configuration.database.runtime.redis.address` | Redis server address in `host:port` format (for Redis only) | `""` | +| `configuration.database.runtime.redis.username` | Redis username (for Redis only) | `""` | +| `configuration.database.runtime.redis.password` | Runtime Redis password. When `passwordRef.key` is set, this field is ignored and the external Secret is used instead. | `""` | +| `configuration.database.runtime.redis.passwordRef.name` | Kubernetes Secret name for runtime Redis password | `""` | +| `configuration.database.runtime.redis.passwordRef.key` | Kubernetes Secret key for runtime Redis password | `""` | +| `configuration.database.runtime.redis.db` | Redis logical database index (0–15) (for Redis only) | `0` | +| `configuration.database.runtime.redis.key_prefix` | Prefix applied to all Redis keys written by Thunder (for Redis only) | `""` | | `configuration.database.user.type` | User database type (postgres or sqlite) | `postgres` | -| `configuration.database.user.sqlitePath` | SQLite database path (for SQLite only) | `repository/database/userdb.db` | -| `configuration.database.user.sqliteOptions` | SQLite options (for SQLite only) | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | -| `configuration.database.user.name` | Postgres database name (for postgres only) | `userdb` | -| `configuration.database.user.host` | Postgres host (for postgres only) | `localhost` | -| `configuration.database.user.port` | Postgres port (for postgres only) | `5432` | -| `configuration.database.user.username` | Postgres username (for postgres only) | `asgthunder` | -| `configuration.database.user.password` | Database password - supports plaintext. When `passwordRef.key` is set, this field is ignored and the external Secret is used instead. | `asgthunder` | -| `configuration.database.user.passwordRef.name` | Kubernetes Secret name for user database password. Leave empty to use auto-created `-db-credentials` Secret when password field is set | `""` | -| `configuration.database.user.passwordRef.key` | Kubernetes Secret key for user database password. When set, overrides `password` field and uses external Secret | `""` | -| `configuration.database.user.sslmode` | Postgres SSL mode (for postgres only) | `require` | -| `configuration.database.user.max_open_conns` | Maximum number of open connections to the database | `500` | -| `configuration.database.user.max_idle_conns` | Maximum number of idle connections in the pool | `100` | -| `configuration.database.user.conn_max_lifetime` | Maximum lifetime of a connection in seconds | `3600` | +| `configuration.database.user.sqlite.path` | SQLite database path (for SQLite only) | `repository/database/userdb.db` | +| `configuration.database.user.sqlite.options` | SQLite options (for SQLite only) | `_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)` | +| `configuration.database.user.sqlite.max_open_conns` | Maximum number of open connections for SQLite | `500` | +| `configuration.database.user.sqlite.max_idle_conns` | Maximum number of idle SQLite connections | `100` | +| `configuration.database.user.sqlite.conn_max_lifetime` | Maximum SQLite connection lifetime in seconds | `3600` | +| `configuration.database.user.postgres.name` | Postgres database name (for postgres only) | `userdb` | +| `configuration.database.user.postgres.hostname` | Postgres hostname (for postgres only) | `localhost` | +| `configuration.database.user.postgres.port` | Postgres port (for postgres only) | `5432` | +| `configuration.database.user.postgres.username` | Postgres username (for postgres only) | `asgthunder` | +| `configuration.database.user.postgres.password` | User Postgres password - supports plaintext. When `passwordRef.key` is set, this field is ignored and the external Secret is used instead. | `asgthunder` | +| `configuration.database.user.postgres.passwordRef.name` | Kubernetes Secret name for user Postgres password | `""` | +| `configuration.database.user.postgres.passwordRef.key` | Kubernetes Secret key for user Postgres password | `""` | +| `configuration.database.user.postgres.sslmode` | Postgres SSL mode (for postgres only) | `require` | +| `configuration.database.user.postgres.max_open_conns` | Maximum number of open connections to the database | `500` | +| `configuration.database.user.postgres.max_idle_conns` | Maximum number of idle connections in the pool | `100` | +| `configuration.database.user.postgres.conn_max_lifetime` | Maximum lifetime of a connection in seconds | `3600` | | `configuration.cache.disabled` | Disable cache | `true` | | `configuration.cache.type` | Cache type | `inmemory` | | `configuration.cache.size` | Cache size | `1000` | diff --git a/install/helm/conf/deployment.yaml b/install/helm/conf/deployment.yaml index abc35ce3d..a1f0cb24e 100644 --- a/install/helm/conf/deployment.yaml +++ b/install/helm/conf/deployment.yaml @@ -60,42 +60,73 @@ database: config: type: {{ .Values.configuration.database.config.type | quote }} {{- if eq .Values.configuration.database.config.type "sqlite" }} - path: {{ .Values.configuration.database.config.sqlitePath | quote }} - options: {{ .Values.configuration.database.config.sqliteOptions | quote }} + sqlite: + path: {{ .Values.configuration.database.config.sqlite.path | quote }} + options: {{ .Values.configuration.database.config.sqlite.options | quote }} + max_open_conns: {{ .Values.configuration.database.config.sqlite.max_open_conns }} + max_idle_conns: {{ .Values.configuration.database.config.sqlite.max_idle_conns }} + conn_max_lifetime: {{ .Values.configuration.database.config.sqlite.conn_max_lifetime }} {{- else }} - hostname: {{ .Values.configuration.database.config.host | quote }} - port: {{ .Values.configuration.database.config.port }} - name: {{ .Values.configuration.database.config.name | quote }} - username: {{ .Values.configuration.database.config.username | quote }} - password: {{ "{{.DB_CONFIG_PASSWORD}}" | quote }} - sslmode: {{ .Values.configuration.database.config.sslmode | quote }} + postgres: + hostname: {{ .Values.configuration.database.config.postgres.hostname | quote }} + port: {{ .Values.configuration.database.config.postgres.port }} + name: {{ .Values.configuration.database.config.postgres.name | quote }} + username: {{ .Values.configuration.database.config.postgres.username | quote }} + password: {{ "{{.DB_CONFIG_PASSWORD}}" | quote }} + sslmode: {{ .Values.configuration.database.config.postgres.sslmode | quote }} + max_open_conns: {{ .Values.configuration.database.config.postgres.max_open_conns }} + max_idle_conns: {{ .Values.configuration.database.config.postgres.max_idle_conns }} + conn_max_lifetime: {{ .Values.configuration.database.config.postgres.conn_max_lifetime }} {{- end }} runtime: type: {{ .Values.configuration.database.runtime.type | quote }} {{- if eq .Values.configuration.database.runtime.type "sqlite" }} - path: {{ .Values.configuration.database.runtime.sqlitePath | quote }} - options: {{ .Values.configuration.database.runtime.sqliteOptions | quote }} + sqlite: + path: {{ .Values.configuration.database.runtime.sqlite.path | quote }} + options: {{ .Values.configuration.database.runtime.sqlite.options | quote }} + max_open_conns: {{ .Values.configuration.database.runtime.sqlite.max_open_conns }} + max_idle_conns: {{ .Values.configuration.database.runtime.sqlite.max_idle_conns }} + conn_max_lifetime: {{ .Values.configuration.database.runtime.sqlite.conn_max_lifetime }} + {{- else if eq .Values.configuration.database.runtime.type "redis" }} + redis: + address: {{ .Values.configuration.database.runtime.redis.address | quote }} + username: {{ .Values.configuration.database.runtime.redis.username | quote }} + password: {{ "{{.DB_RUNTIME_REDIS_PASSWORD}}" | quote }} + db: {{ .Values.configuration.database.runtime.redis.db }} + key_prefix: {{ .Values.configuration.database.runtime.redis.key_prefix | quote }} {{- else }} - hostname: {{ .Values.configuration.database.runtime.host | quote }} - port: {{ .Values.configuration.database.runtime.port }} - name: {{ .Values.configuration.database.runtime.name | quote }} - username: {{ .Values.configuration.database.runtime.username | quote }} - password: {{ "{{.DB_RUNTIME_PASSWORD}}" | quote }} - sslmode: {{ .Values.configuration.database.runtime.sslmode | quote }} + postgres: + hostname: {{ .Values.configuration.database.runtime.postgres.hostname | quote }} + port: {{ .Values.configuration.database.runtime.postgres.port }} + name: {{ .Values.configuration.database.runtime.postgres.name | quote }} + username: {{ .Values.configuration.database.runtime.postgres.username | quote }} + password: {{ "{{.DB_RUNTIME_PASSWORD}}" | quote }} + sslmode: {{ .Values.configuration.database.runtime.postgres.sslmode | quote }} + max_open_conns: {{ .Values.configuration.database.runtime.postgres.max_open_conns }} + max_idle_conns: {{ .Values.configuration.database.runtime.postgres.max_idle_conns }} + conn_max_lifetime: {{ .Values.configuration.database.runtime.postgres.conn_max_lifetime }} {{- end }} user: type: {{ .Values.configuration.database.user.type | quote }} {{- if eq .Values.configuration.database.user.type "sqlite" }} - path: {{ .Values.configuration.database.user.sqlitePath | quote }} - options: {{ .Values.configuration.database.user.sqliteOptions | quote }} + sqlite: + path: {{ .Values.configuration.database.user.sqlite.path | quote }} + options: {{ .Values.configuration.database.user.sqlite.options | quote }} + max_open_conns: {{ .Values.configuration.database.user.sqlite.max_open_conns }} + max_idle_conns: {{ .Values.configuration.database.user.sqlite.max_idle_conns }} + conn_max_lifetime: {{ .Values.configuration.database.user.sqlite.conn_max_lifetime }} {{- else }} - hostname: {{ .Values.configuration.database.user.host | quote }} - port: {{ .Values.configuration.database.user.port }} - name: {{ .Values.configuration.database.user.name | quote }} - username: {{ .Values.configuration.database.user.username | quote }} - password: {{ "{{.DB_USER_PASSWORD}}" | quote }} - sslmode: {{ .Values.configuration.database.user.sslmode | quote }} - {{- end }} + postgres: + hostname: {{ .Values.configuration.database.user.postgres.hostname | quote }} + port: {{ .Values.configuration.database.user.postgres.port }} + name: {{ .Values.configuration.database.user.postgres.name | quote }} + username: {{ .Values.configuration.database.user.postgres.username | quote }} + password: {{ "{{.DB_USER_PASSWORD}}" | quote }} + sslmode: {{ .Values.configuration.database.user.postgres.sslmode | quote }} + max_open_conns: {{ .Values.configuration.database.user.postgres.max_open_conns }} + max_idle_conns: {{ .Values.configuration.database.user.postgres.max_idle_conns }} + conn_max_lifetime: {{ .Values.configuration.database.user.postgres.conn_max_lifetime }} + {{- end }} cache: disabled: {{ .Values.configuration.cache.disabled }} diff --git a/install/helm/templates/_helpers.tpl b/install/helm/templates/_helpers.tpl index c2e2db3bd..e8a8fb533 100644 --- a/install/helm/templates/_helpers.tpl +++ b/install/helm/templates/_helpers.tpl @@ -91,11 +91,15 @@ This is used to trigger pod restarts when auto-generated Secrets change. {{- $config := default dict $database.config -}} {{- $runtime := default dict $database.runtime -}} {{- $user := default dict $database.user -}} +{{- $configPostgres := default dict $config.postgres -}} +{{- $runtimePostgres := default dict $runtime.postgres -}} +{{- $runtimeRedis := default dict $runtime.redis -}} +{{- $userPostgres := default dict $user.postgres -}} {{- $consent := default dict $configuration.consent -}} {{- $consentDb := default dict $consent.database -}} {{- $cache := default dict $configuration.cache -}} {{- $redis := default dict $cache.redis -}} -{{- if or (and $config.password (not (default dict $config.passwordRef).key)) (and $runtime.password (not (default dict $runtime.passwordRef).key)) (and $user.password (not (default dict $user.passwordRef).key)) (and $consent.enabled $consentDb.password (not (default dict $consentDb.passwordRef).key)) (and $redis.password (eq $cache.type "redis") (not (default dict $redis.passwordRef).key)) }}true{{- end }} +{{- if or (and $configPostgres.password (not (default dict $configPostgres.passwordRef).key)) (and $runtimePostgres.password (not (default dict $runtimePostgres.passwordRef).key)) (and $runtimeRedis.password (not (default dict $runtimeRedis.passwordRef).key)) (and $userPostgres.password (not (default dict $userPostgres.passwordRef).key)) (and $consent.enabled $consentDb.password (not (default dict $consentDb.passwordRef).key)) (and $redis.password (eq $cache.type "redis") (not (default dict $redis.passwordRef).key)) }}true{{- end }} {{- end }} {{/* @@ -109,27 +113,39 @@ Injects DB_CONFIG_PASSWORD, DB_RUNTIME_PASSWORD, and DB_USER_PASSWORD from eithe {{- $config := default dict $database.config -}} {{- $runtime := default dict $database.runtime -}} {{- $user := default dict $database.user -}} +{{- $configPostgres := default dict $config.postgres -}} +{{- $runtimePostgres := default dict $runtime.postgres -}} +{{- $runtimeRedis := default dict $runtime.redis -}} +{{- $userPostgres := default dict $user.postgres -}} {{- $consent := default dict $configuration.consent -}} {{- $consentDb := default dict $consent.database -}} -{{- $configPasswordRef := default dict $config.passwordRef -}} -{{- $runtimePasswordRef := default dict $runtime.passwordRef -}} -{{- $userPasswordRef := default dict $user.passwordRef -}} +{{- $configPasswordRef := default dict $configPostgres.passwordRef -}} +{{- $runtimePasswordRef := default dict $runtimePostgres.passwordRef -}} +{{- $runtimeRedisPasswordRef := default dict $runtimeRedis.passwordRef -}} +{{- $userPasswordRef := default dict $userPostgres.passwordRef -}} {{- $consentPasswordRef := default dict $consentDb.passwordRef -}} -{{- if or $config.password $configPasswordRef.key }} +{{- if or $configPostgres.password $configPasswordRef.key }} - name: DB_CONFIG_PASSWORD valueFrom: secretKeyRef: name: {{ if $configPasswordRef.key }}{{ $configPasswordRef.name | default $defaultDbSecretName }}{{ else }}{{ $defaultDbSecretName }}{{ end }} key: {{ $configPasswordRef.key | default "config-db-password" }} {{- end }} -{{- if or $runtime.password $runtimePasswordRef.key }} +{{- if or $runtimePostgres.password $runtimePasswordRef.key }} - name: DB_RUNTIME_PASSWORD valueFrom: secretKeyRef: name: {{ if $runtimePasswordRef.key }}{{ $runtimePasswordRef.name | default $defaultDbSecretName }}{{ else }}{{ $defaultDbSecretName }}{{ end }} key: {{ $runtimePasswordRef.key | default "runtime-db-password" }} {{- end }} -{{- if or $user.password $userPasswordRef.key }} +{{- if or $runtimeRedis.password $runtimeRedisPasswordRef.key }} +- name: DB_RUNTIME_REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ if $runtimeRedisPasswordRef.key }}{{ $runtimeRedisPasswordRef.name | default $defaultDbSecretName }}{{ else }}{{ $defaultDbSecretName }}{{ end }} + key: {{ $runtimeRedisPasswordRef.key | default "runtime-redis-password" }} +{{- end }} +{{- if or $userPostgres.password $userPasswordRef.key }} - name: DB_USER_PASSWORD valueFrom: secretKeyRef: diff --git a/install/helm/templates/secret.yaml b/install/helm/templates/secret.yaml index fb78246c8..ac3d70d01 100644 --- a/install/helm/templates/secret.yaml +++ b/install/helm/templates/secret.yaml @@ -19,6 +19,10 @@ {{- $config := default dict $database.config -}} {{- $runtime := default dict $database.runtime -}} {{- $user := default dict $database.user -}} +{{- $configPostgres := default dict $config.postgres -}} +{{- $runtimePostgres := default dict $runtime.postgres -}} +{{- $runtimeRedis := default dict $runtime.redis -}} +{{- $userPostgres := default dict $user.postgres -}} {{- $consent := default dict $configuration.consent -}} {{- $consentDb := default dict $consent.database -}} {{- $cache := default dict $configuration.cache -}} @@ -26,18 +30,22 @@ {{- $configPassword := "" }} {{- $runtimePassword := "" }} +{{- $runtimeRedisPassword := "" }} {{- $userPassword := "" }} {{- $consentPassword := "" }} {{- $redisPassword := "" }} -{{- if and $config.password (not (default dict $config.passwordRef).key) }} - {{- $configPassword = $config.password }} +{{- if and $configPostgres.password (not (default dict $configPostgres.passwordRef).key) }} + {{- $configPassword = $configPostgres.password }} {{- end }} -{{- if and $runtime.password (not (default dict $runtime.passwordRef).key) }} - {{- $runtimePassword = $runtime.password }} +{{- if and $runtimePostgres.password (not (default dict $runtimePostgres.passwordRef).key) }} + {{- $runtimePassword = $runtimePostgres.password }} {{- end }} -{{- if and $user.password (not (default dict $user.passwordRef).key) }} - {{- $userPassword = $user.password }} +{{- if and $runtimeRedis.password (not (default dict $runtimeRedis.passwordRef).key) }} + {{- $runtimeRedisPassword = $runtimeRedis.password }} +{{- end }} +{{- if and $userPostgres.password (not (default dict $userPostgres.passwordRef).key) }} + {{- $userPassword = $userPostgres.password }} {{- end }} {{- if and $consent.enabled $consentDb.password (not (default dict $consentDb.passwordRef).key) }} {{- $consentPassword = $consentDb.password }} @@ -46,7 +54,7 @@ {{- $redisPassword = $redis.password }} {{- end }} -{{- $createSecret := or $configPassword $runtimePassword $userPassword $consentPassword $redisPassword}} +{{- $createSecret := or $configPassword $runtimePassword $runtimeRedisPassword $userPassword $consentPassword $redisPassword}} {{- if $createSecret }} apiVersion: v1 @@ -71,6 +79,9 @@ data: {{- if $runtimePassword }} runtime-db-password: {{ $runtimePassword | b64enc | quote }} {{- end }} + {{- if $runtimeRedisPassword }} + runtime-redis-password: {{ $runtimeRedisPassword | b64enc | quote }} + {{- end }} {{- if $userPassword }} user-db-password: {{ $userPassword | b64enc | quote }} {{- end }} diff --git a/install/helm/values.yaml b/install/helm/values.yaml index 378a21f5c..f66cd4447 100644 --- a/install/helm/values.yaml +++ b/install/helm/values.yaml @@ -204,52 +204,79 @@ configuration: config: # WARNING: Use sqlite only if you are running a single pod. type: "postgres" # postgres or sqlite - # Below two parameters are only required for sqlite - sqlitePath: "repository/database/configdb.db" # Only required for sqlite - sqliteOptions: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" # Only required for sqlite - # Below parameters are only required for Postgres - name: "configdb" - host: "localhost" - port: "5432" - username: "asgthunder" - password: "asgthunder" - sslmode: "require" - max_open_conns: 500 - max_idle_conns: 100 - conn_max_lifetime: 3600 + postgres: + hostname: "localhost" + port: "5432" + name: "configdb" + username: "asgthunder" + password: "asgthunder" + passwordRef: + name: "" + key: "" + sslmode: "require" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 + sqlite: + path: "repository/database/configdb.db" + options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 # Runtime database configuration runtime: # WARNING: Use sqlite only if you are running a single pod. - type: "postgres" # postgres or sqlite - # Below two parameters are only required for sqlite - sqlitePath: "repository/database/runtimedb.db" # Only required for sqlite - sqliteOptions: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" # Only required for sqlite - # Below parameters are only required for Postgres - name: "runtimedb" - host: "localhost" - port: "5432" - username: "asgthunder" - password: "asgthunder" - sslmode: "require" - max_open_conns: 500 - max_idle_conns: 100 - conn_max_lifetime: 3600 + type: "postgres" # postgres, sqlite, or redis + postgres: + hostname: "localhost" + port: "5432" + name: "runtimedb" + username: "asgthunder" + password: "asgthunder" + passwordRef: + name: "" + key: "" + sslmode: "require" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 + sqlite: + path: "repository/database/runtimedb.db" + options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 + redis: + address: "" # Redis server address, e.g. "localhost:6379" + username: "" + password: "" + passwordRef: + name: "" + key: "" + db: 0 # Redis logical database index (0-15) + key_prefix: "" # Prefix for all Redis keys, e.g. "thunder:" user: # WARNING: Use sqlite only if you are running a single pod. type: "postgres" # postgres or sqlite - # Below two parameters are only required for sqlite - sqlitePath: "repository/database/userdb.db" # Only required for sqlite - sqliteOptions: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" # Only required for sqlite - # Below parameters are only required for Postgres - name: "userdb" - host: "localhost" - port: "5432" - username: "asgthunder" - password: "asgthunder" - sslmode: "require" - max_open_conns: 500 - max_idle_conns: 100 - conn_max_lifetime: 3600 + postgres: + hostname: "localhost" + port: "5432" + name: "userdb" + username: "asgthunder" + password: "asgthunder" + passwordRef: + name: "" + key: "" + sslmode: "require" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 + sqlite: + path: "repository/database/userdb.db" + options: "_journal_mode=WAL&_busy_timeout=5000&_pragma=foreign_keys(1)" + max_open_conns: 500 + max_idle_conns: 100 + conn_max_lifetime: 3600 # Cache configuration cache: diff --git a/install/openchoreo/helm/templates/_helpers.tpl b/install/openchoreo/helm/templates/_helpers.tpl index 8943aff79..8d97afe3d 100644 --- a/install/openchoreo/helm/templates/_helpers.tpl +++ b/install/openchoreo/helm/templates/_helpers.tpl @@ -191,28 +191,31 @@ resources: database: config: type: "postgres" - hostname: "${environmentConfigs.configDbHostname}" - port: ${parameters.initial.database.config.port} - name: "${environmentConfigs.configDbName}" - username: "${environmentConfigs.configDbUsername}" - password: "${environmentConfigs.configDbPassword}" - sslmode: "${parameters.initial.database.config.sslmode}" + postgres: + hostname: "${environmentConfigs.configDbHostname}" + port: ${parameters.initial.database.config.port} + name: "${environmentConfigs.configDbName}" + username: "${environmentConfigs.configDbUsername}" + password: "${environmentConfigs.configDbPassword}" + sslmode: "${parameters.initial.database.config.sslmode}" runtime: type: "postgres" - hostname: "${environmentConfigs.runtimeDbHostname}" - port: ${parameters.initial.database.runtime.port} - name: "${environmentConfigs.runtimeDbName}" - username: "${environmentConfigs.runtimeDbUsername}" - password: "${environmentConfigs.runtimeDbPassword}" - sslmode: "${parameters.initial.database.runtime.sslmode}" + postgres: + hostname: "${environmentConfigs.runtimeDbHostname}" + port: ${parameters.initial.database.runtime.port} + name: "${environmentConfigs.runtimeDbName}" + username: "${environmentConfigs.runtimeDbUsername}" + password: "${environmentConfigs.runtimeDbPassword}" + sslmode: "${parameters.initial.database.runtime.sslmode}" user: type: "postgres" - hostname: "${environmentConfigs.userDbHostname}" - port: ${parameters.initial.database.user.port} - name: "${environmentConfigs.userDbName}" - username: "${environmentConfigs.userDbUsername}" - password: "${environmentConfigs.userDbPassword}" - sslmode: "${parameters.initial.database.user.sslmode}" + postgres: + hostname: "${environmentConfigs.userDbHostname}" + port: ${parameters.initial.database.user.port} + name: "${environmentConfigs.userDbName}" + username: "${environmentConfigs.userDbUsername}" + password: "${environmentConfigs.userDbPassword}" + sslmode: "${parameters.initial.database.user.sslmode}" cache: disabled: false diff --git a/install/openchoreo/helm/templates/setup-configmap.yaml b/install/openchoreo/helm/templates/setup-configmap.yaml index 3163e8634..71d8c255c 100644 --- a/install/openchoreo/helm/templates/setup-configmap.yaml +++ b/install/openchoreo/helm/templates/setup-configmap.yaml @@ -34,25 +34,28 @@ data: database: config: type: "postgres" - hostname: {{ .Values.database.host | quote }} - port: {{ .Values.database.port }} - name: {{ .Values.database.config.database | quote }} - username: {{ .Values.database.config.username | quote }} - password: {{ .Values.database.config.password | quote }} - sslmode: {{ .Values.database.config.sslmode | quote }} + postgres: + hostname: {{ .Values.database.host | quote }} + port: {{ .Values.database.port }} + name: {{ .Values.database.config.database | quote }} + username: {{ .Values.database.config.username | quote }} + password: {{ .Values.database.config.password | quote }} + sslmode: {{ .Values.database.config.sslmode | quote }} runtime: type: "postgres" - hostname: {{ .Values.database.host | quote }} - port: {{ .Values.database.port }} - name: {{ .Values.database.runtime.database | quote }} - username: {{ .Values.database.runtime.username | quote }} - password: {{ .Values.database.runtime.password | quote }} - sslmode: {{ .Values.database.runtime.sslmode | quote }} + postgres: + hostname: {{ .Values.database.host | quote }} + port: {{ .Values.database.port }} + name: {{ .Values.database.runtime.database | quote }} + username: {{ .Values.database.runtime.username | quote }} + password: {{ .Values.database.runtime.password | quote }} + sslmode: {{ .Values.database.runtime.sslmode | quote }} user: type: "postgres" - hostname: {{ .Values.database.host | quote }} - port: {{ .Values.database.port }} - name: {{ .Values.database.user.database | quote }} - username: {{ .Values.database.user.username | quote }} - password: {{ .Values.database.user.password | quote }} - sslmode: {{ .Values.database.user.sslmode | quote }} + postgres: + hostname: {{ .Values.database.host | quote }} + port: {{ .Values.database.port }} + name: {{ .Values.database.user.database | quote }} + username: {{ .Values.database.user.username | quote }} + password: {{ .Values.database.user.password | quote }} + sslmode: {{ .Values.database.user.sslmode | quote }} diff --git a/tests/integration/resources/deployment.yaml b/tests/integration/resources/deployment.yaml index c41b278eb..05562bad8 100644 --- a/tests/integration/resources/deployment.yaml +++ b/tests/integration/resources/deployment.yaml @@ -9,16 +9,19 @@ tls: database: config: type: "sqlite" - path: "repository/database/configdb.db" - options: "_journal_mode=WAL&_busy_timeout=60000&_pragma=foreign_keys(1)" + sqlite: + path: "repository/database/configdb.db" + options: "_journal_mode=WAL&_busy_timeout=60000&_pragma=foreign_keys(1)" runtime: type: "sqlite" - path: "repository/database/runtimedb.db" - options: "_journal_mode=WAL&_busy_timeout=60000&_pragma=foreign_keys(1)" + sqlite: + path: "repository/database/runtimedb.db" + options: "_journal_mode=WAL&_busy_timeout=60000&_pragma=foreign_keys(1)" user: type: "sqlite" - path: "repository/database/userdb.db" - options: "_journal_mode=WAL&_busy_timeout=60000&_pragma=foreign_keys(1)" + sqlite: + path: "repository/database/userdb.db" + options: "_journal_mode=WAL&_busy_timeout=60000&_pragma=foreign_keys(1)" flow: max_version_history: 3 diff --git a/tests/integration/resources/scripts/setup-test-config.sh b/tests/integration/resources/scripts/setup-test-config.sh index fd8a033a8..d216525a4 100644 --- a/tests/integration/resources/scripts/setup-test-config.sh +++ b/tests/integration/resources/scripts/setup-test-config.sh @@ -19,71 +19,74 @@ if [ "$DB_TYPE" = "postgres" ]; then cat >> tests/integration/resources/deployment.yaml <> tests/integration/resources/deployment.yaml <> tests/integration/resources/deployment.yaml <