feat: cache OIDC metadata & JWKS when read by authorized integration (#12275)

Enhances authorized integrations (#12261) with a cache of the remote OpenID Connect descriptor file and JSON Web Key Set (JWKS), improving runtime performance and reducing intermittent reliability risks.  By default a 10 minute cache is used, configurable through `[authorized_integration].CACHE_TTL`.

To mock the cache for testing, mockery code generation is added, and a previous manually generated mock for `AuthorizationReducer` was replaced with the code generation.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests for Go changes

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [x] `make pr-go` before pushing

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.
    - Authorized integrations are not yet exposed to end-users.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12275
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Mathieu Fenniak 2026-04-28 02:13:06 +02:00 committed by Mathieu Fenniak
commit 37412e6a00
15 changed files with 2123 additions and 429 deletions

View file

@ -7,18 +7,17 @@ import (
"context"
"testing"
"forgejo.org/modules/queue/mock"
"forgejo.org/modules/nosql"
queue_mock "forgejo.org/modules/queue/mock"
"forgejo.org/modules/setting"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
)
type baseRedisUnitTestSuite struct {
suite.Suite
mockController *gomock.Controller
}
func TestBaseRedis(t *testing.T) {
@ -26,7 +25,6 @@ func TestBaseRedis(t *testing.T) {
}
func (suite *baseRedisUnitTestSuite) SetupSuite() {
suite.mockController = gomock.NewController(suite.T())
}
func (suite *baseRedisUnitTestSuite) TestBasic() {
@ -71,39 +69,47 @@ func (suite *baseRedisUnitTestSuite) TestBasic() {
}
// Configure expectations.
mockRedisStore := mock.NewInMemoryMockRedis()
redisClient := mock.NewMockRedisClient(suite.mockController)
mockRedisStore := queue_mock.NewInMemoryMockRedis()
redisClient := nosql.NewMockRedisClient(suite.T())
redisClient.EXPECT().
Ping(gomock.Any()).
Times(1).
Return(&redis.StatusCmd{})
Ping(mock.Anything).
Return(&redis.StatusCmd{}).
Times(1)
redisClient.EXPECT().
LLen(gomock.Any(), testCase.QueueName).
Times(1).
DoAndReturn(mockRedisStore.LLen)
LLen(mock.Anything, testCase.QueueName).
RunAndReturn(mockRedisStore.LLen).
Times(1)
redisClient.EXPECT().
LPop(gomock.Any(), testCase.QueueName).
Times(1).
DoAndReturn(mockRedisStore.LPop)
LPop(mock.Anything, testCase.QueueName).
RunAndReturn(mockRedisStore.LPop).
Times(1)
redisClient.EXPECT().
RPush(gomock.Any(), testCase.QueueName, gomock.Any()).
Times(1).
DoAndReturn(mockRedisStore.RPush)
RPush(mock.Anything, testCase.QueueName, mock.Anything).
RunAndReturn(func(ctx context.Context, key string, values ...any) *redis.IntCmd {
return mockRedisStore.RPush(ctx, key, values[0].([]byte))
}).
Times(1)
if testCase.Unique {
redisClient.EXPECT().
SAdd(gomock.Any(), testCase.QueueName+"_unique", gomock.Any()).
Times(1).
DoAndReturn(mockRedisStore.SAdd)
SAdd(mock.Anything, testCase.QueueName+"_unique", mock.Anything).
RunAndReturn(func(ctx context.Context, key string, members ...any) *redis.IntCmd {
return mockRedisStore.SAdd(ctx, key, members[0].([]byte))
}).
Times(1)
redisClient.EXPECT().
SRem(gomock.Any(), testCase.QueueName+"_unique", gomock.Any()).
Times(1).
DoAndReturn(mockRedisStore.SRem)
SRem(mock.Anything, testCase.QueueName+"_unique", mock.Anything).
RunAndReturn(func(ctx context.Context, key string, members ...any) *redis.IntCmd {
return mockRedisStore.SRem(ctx, key, members[0].([]byte))
}).
Times(1)
redisClient.EXPECT().
SIsMember(gomock.Any(), testCase.QueueName+"_unique", gomock.Any()).
Times(2).
DoAndReturn(mockRedisStore.SIsMember)
SIsMember(mock.Anything, testCase.QueueName+"_unique", mock.Anything).
RunAndReturn(func(ctx context.Context, key string, member any) *redis.BoolCmd {
return mockRedisStore.SIsMember(ctx, key, member.([]byte))
}).
Times(2)
}
client, err := newBaseRedisGeneric(

View file

@ -1,344 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: forgejo.org/modules/nosql (interfaces: RedisClient)
//
// Generated by this command:
//
// mockgen -package mock -destination ./modules/queue/mock/redisuniversalclient.go forgejo.org/modules/nosql RedisClient
//
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
time "time"
redis "github.com/redis/go-redis/v9"
gomock "go.uber.org/mock/gomock"
)
// MockRedisClient is a mock of RedisClient interface.
type MockRedisClient struct {
ctrl *gomock.Controller
recorder *MockRedisClientMockRecorder
isgomock struct{}
}
// MockRedisClientMockRecorder is the mock recorder for MockRedisClient.
type MockRedisClientMockRecorder struct {
mock *MockRedisClient
}
// NewMockRedisClient creates a new mock instance.
func NewMockRedisClient(ctrl *gomock.Controller) *MockRedisClient {
mock := &MockRedisClient{ctrl: ctrl}
mock.recorder = &MockRedisClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRedisClient) EXPECT() *MockRedisClientMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockRedisClient) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockRedisClientMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRedisClient)(nil).Close))
}
// DBSize mocks base method.
func (m *MockRedisClient) DBSize(ctx context.Context) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DBSize", ctx)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// DBSize indicates an expected call of DBSize.
func (mr *MockRedisClientMockRecorder) DBSize(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBSize", reflect.TypeOf((*MockRedisClient)(nil).DBSize), ctx)
}
// Decr mocks base method.
func (m *MockRedisClient) Decr(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Decr", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Decr indicates an expected call of Decr.
func (mr *MockRedisClientMockRecorder) Decr(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decr", reflect.TypeOf((*MockRedisClient)(nil).Decr), ctx, key)
}
// Del mocks base method.
func (m *MockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Del", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Del indicates an expected call of Del.
func (mr *MockRedisClientMockRecorder) Del(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Del", reflect.TypeOf((*MockRedisClient)(nil).Del), varargs...)
}
// Exists mocks base method.
func (m *MockRedisClient) Exists(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exists", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Exists indicates an expected call of Exists.
func (mr *MockRedisClientMockRecorder) Exists(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockRedisClient)(nil).Exists), varargs...)
}
// FlushDB mocks base method.
func (m *MockRedisClient) FlushDB(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FlushDB", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FlushDB indicates an expected call of FlushDB.
func (mr *MockRedisClientMockRecorder) FlushDB(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushDB", reflect.TypeOf((*MockRedisClient)(nil).FlushDB), ctx)
}
// Get mocks base method.
func (m *MockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockRedisClientMockRecorder) Get(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRedisClient)(nil).Get), ctx, key)
}
// HDel mocks base method.
func (m *MockRedisClient) HDel(ctx context.Context, key string, fields ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HDel", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HDel indicates an expected call of HDel.
func (mr *MockRedisClientMockRecorder) HDel(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HDel", reflect.TypeOf((*MockRedisClient)(nil).HDel), varargs...)
}
// HKeys mocks base method.
func (m *MockRedisClient) HKeys(ctx context.Context, key string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HKeys", ctx, key)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// HKeys indicates an expected call of HKeys.
func (mr *MockRedisClientMockRecorder) HKeys(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HKeys", reflect.TypeOf((*MockRedisClient)(nil).HKeys), ctx, key)
}
// HSet mocks base method.
func (m *MockRedisClient) HSet(ctx context.Context, key string, values ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HSet", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HSet indicates an expected call of HSet.
func (mr *MockRedisClientMockRecorder) HSet(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HSet", reflect.TypeOf((*MockRedisClient)(nil).HSet), varargs...)
}
// Incr mocks base method.
func (m *MockRedisClient) Incr(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Incr", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Incr indicates an expected call of Incr.
func (mr *MockRedisClientMockRecorder) Incr(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Incr", reflect.TypeOf((*MockRedisClient)(nil).Incr), ctx, key)
}
// LLen mocks base method.
func (m *MockRedisClient) LLen(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LLen", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LLen indicates an expected call of LLen.
func (mr *MockRedisClientMockRecorder) LLen(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LLen", reflect.TypeOf((*MockRedisClient)(nil).LLen), ctx, key)
}
// LPop mocks base method.
func (m *MockRedisClient) LPop(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LPop", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// LPop indicates an expected call of LPop.
func (mr *MockRedisClientMockRecorder) LPop(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPop", reflect.TypeOf((*MockRedisClient)(nil).LPop), ctx, key)
}
// Ping mocks base method.
func (m *MockRedisClient) Ping(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Ping indicates an expected call of Ping.
func (mr *MockRedisClientMockRecorder) Ping(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockRedisClient)(nil).Ping), ctx)
}
// RPush mocks base method.
func (m *MockRedisClient) RPush(ctx context.Context, key string, values ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "RPush", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// RPush indicates an expected call of RPush.
func (mr *MockRedisClientMockRecorder) RPush(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPush", reflect.TypeOf((*MockRedisClient)(nil).RPush), varargs...)
}
// SAdd mocks base method.
func (m *MockRedisClient) SAdd(ctx context.Context, key string, members ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SAdd", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SAdd indicates an expected call of SAdd.
func (mr *MockRedisClientMockRecorder) SAdd(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SAdd", reflect.TypeOf((*MockRedisClient)(nil).SAdd), varargs...)
}
// SIsMember mocks base method.
func (m *MockRedisClient) SIsMember(ctx context.Context, key string, member any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SIsMember", ctx, key, member)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// SIsMember indicates an expected call of SIsMember.
func (mr *MockRedisClientMockRecorder) SIsMember(ctx, key, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SIsMember", reflect.TypeOf((*MockRedisClient)(nil).SIsMember), ctx, key, member)
}
// SRem mocks base method.
func (m *MockRedisClient) SRem(ctx context.Context, key string, members ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SRem", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SRem indicates an expected call of SRem.
func (mr *MockRedisClientMockRecorder) SRem(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SRem", reflect.TypeOf((*MockRedisClient)(nil).SRem), varargs...)
}
// Set mocks base method.
func (m *MockRedisClient) Set(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", ctx, key, value, expiration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockRedisClientMockRecorder) Set(ctx, key, value, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockRedisClient)(nil).Set), ctx, key, value, expiration)
}