mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
f52c8db21a
- 新增 cache-framework skill,规范 RAM→Redis→DB 读路径、失效与 pub/sub - 上传元数据 Otter+Redis 缓存与多节点失效;Auth Source 缓存与 pub/sub - ListSystemConfigsByKeys 补 Redis 层;上传统计单事务;登录/Token 缓存预热 - cleanup 任务补 upload meta 失效钩子
240 lines
7.3 KiB
Go
240 lines
7.3 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package repository
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/redis/go-redis/v9/maintnotifications"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/Rain-kl/Wavelet/internal/db"
|
|
"github.com/Rain-kl/Wavelet/internal/model"
|
|
)
|
|
|
|
func setupAuthSourceCacheTest(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) {
|
|
t.Helper()
|
|
|
|
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
DisableForeignKeyConstraintWhenMigrating: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to open in-memory SQLite db: %v", err)
|
|
}
|
|
if err := sqliteDB.AutoMigrate(&model.AuthSource{}); err != nil {
|
|
t.Fatalf("failed to migrate auth sources: %v", err)
|
|
}
|
|
|
|
miniRedis, err := miniredis.Run()
|
|
if err != nil {
|
|
t.Fatalf("failed to start miniredis: %v", err)
|
|
}
|
|
|
|
db.SetDB(sqliteDB)
|
|
db.Redis = redis.NewClient(&redis.Options{
|
|
Addr: miniRedis.Addr(),
|
|
MaintNotificationsConfig: &maintnotifications.Config{
|
|
Mode: maintnotifications.ModeDisabled,
|
|
},
|
|
})
|
|
|
|
ResetAuthSourceRAMCacheForTest()
|
|
|
|
cleanup := func() {
|
|
StopAuthSourceCacheListener()
|
|
ResetAuthSourceRAMCacheForTest()
|
|
db.Redis.Close()
|
|
miniRedis.Close()
|
|
db.Redis = nil
|
|
}
|
|
return sqliteDB, miniRedis, cleanup
|
|
}
|
|
|
|
func TestGetActiveAuthSourcesCached_LoadsFromRedisBeforeDB(t *testing.T) {
|
|
dbConn, _, cleanup := setupAuthSourceCacheTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
if err := InvalidateAuthSourceCache(ctx); err != nil {
|
|
t.Fatalf("InvalidateAuthSourceCache() error = %v", err)
|
|
}
|
|
|
|
source := model.AuthSource{
|
|
Name: "cached-source",
|
|
Type: model.AuthSourceTypeOIDC,
|
|
DisplayName: "Cached Source",
|
|
IsActive: true,
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
OpenIDDiscoveryURL: "https://issuer.example.com",
|
|
}
|
|
if err := model.CreateAuthSource(ctx, &source); err != nil {
|
|
t.Fatalf("CreateAuthSource() error = %v", err)
|
|
}
|
|
|
|
warmed, err := GetActiveAuthSourcesCached(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetActiveAuthSourcesCached() warm error = %v", err)
|
|
}
|
|
if len(warmed) == 0 || warmed[0].Name != source.Name {
|
|
t.Fatalf("GetActiveAuthSourcesCached() warm = %#v, want source %q", warmed, source.Name)
|
|
}
|
|
|
|
if err := dbConn.Delete(&model.AuthSource{}, "id = ?", source.ID).Error; err != nil {
|
|
t.Fatalf("Delete(auth source) error = %v", err)
|
|
}
|
|
|
|
ResetAuthSourceRAMCacheForTest()
|
|
|
|
cached, err := GetActiveAuthSourcesCached(ctx)
|
|
if err != nil {
|
|
t.Fatalf("GetActiveAuthSourcesCached() cached error = %v", err)
|
|
}
|
|
if len(cached) == 0 || cached[0].Name != source.Name {
|
|
t.Fatalf("GetActiveAuthSourcesCached() = %#v, want redis-backed source %q", cached, source.Name)
|
|
}
|
|
}
|
|
|
|
func TestGetAuthSourceByNameCached_LoadsFromRedisBeforeDB(t *testing.T) {
|
|
dbConn, _, cleanup := setupAuthSourceCacheTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
if err := InvalidateAuthSourceCache(ctx); err != nil {
|
|
t.Fatalf("InvalidateAuthSourceCache() error = %v", err)
|
|
}
|
|
|
|
source := model.AuthSource{
|
|
Name: "by-name-source",
|
|
Type: model.AuthSourceTypeOIDC,
|
|
DisplayName: "By Name Source",
|
|
IsActive: true,
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
OpenIDDiscoveryURL: "https://issuer.example.com",
|
|
}
|
|
if err := model.CreateAuthSource(ctx, &source); err != nil {
|
|
t.Fatalf("CreateAuthSource() error = %v", err)
|
|
}
|
|
|
|
warmed, err := GetAuthSourceByNameCached(ctx, source.Name)
|
|
if err != nil {
|
|
t.Fatalf("GetAuthSourceByNameCached() warm error = %v", err)
|
|
}
|
|
if warmed.Name != source.Name || warmed.ClientSecret != source.ClientSecret {
|
|
t.Fatalf("GetAuthSourceByNameCached() warm = %#v, want %#v", warmed, source)
|
|
}
|
|
|
|
if err := dbConn.Delete(&model.AuthSource{}, "id = ?", source.ID).Error; err != nil {
|
|
t.Fatalf("Delete(auth source) error = %v", err)
|
|
}
|
|
|
|
ResetAuthSourceRAMCacheForTest()
|
|
|
|
cached, err := GetAuthSourceByNameCached(ctx, source.Name)
|
|
if err != nil {
|
|
t.Fatalf("GetAuthSourceByNameCached() cached error = %v", err)
|
|
}
|
|
if cached.Name != source.Name || cached.ClientSecret != source.ClientSecret {
|
|
t.Fatalf("GetAuthSourceByNameCached() = %#v, want redis-backed source %#v", cached, source)
|
|
}
|
|
}
|
|
|
|
func TestInvalidateAuthSourceCache_ClearsRedisKeys(t *testing.T) {
|
|
_, _, cleanup := setupAuthSourceCacheTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
if err := InvalidateAuthSourceCache(ctx); err != nil {
|
|
t.Fatalf("InvalidateAuthSourceCache() initial error = %v", err)
|
|
}
|
|
|
|
source := model.AuthSource{
|
|
Name: "invalidate-source",
|
|
Type: model.AuthSourceTypeOIDC,
|
|
DisplayName: "Invalidate Source",
|
|
IsActive: true,
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
OpenIDDiscoveryURL: "https://issuer.example.com",
|
|
}
|
|
if err := model.CreateAuthSource(ctx, &source); err != nil {
|
|
t.Fatalf("CreateAuthSource() error = %v", err)
|
|
}
|
|
if _, err := GetActiveAuthSourcesCached(ctx); err != nil {
|
|
t.Fatalf("GetActiveAuthSourcesCached() warm error = %v", err)
|
|
}
|
|
if _, err := GetAuthSourceByNameCached(ctx, source.Name); err != nil {
|
|
t.Fatalf("GetAuthSourceByNameCached() warm error = %v", err)
|
|
}
|
|
|
|
if err := InvalidateAuthSourceCache(ctx); err != nil {
|
|
t.Fatalf("InvalidateAuthSourceCache() error = %v", err)
|
|
}
|
|
|
|
activeExists, err := db.Redis.Exists(ctx, db.PrefixedKey(authSourceActiveRedisKey)).Result()
|
|
if err != nil {
|
|
t.Fatalf("Exists(active key) error = %v", err)
|
|
}
|
|
if activeExists != 0 {
|
|
t.Fatalf("active redis key still exists after invalidation")
|
|
}
|
|
|
|
byNameExists, err := db.Redis.Exists(ctx, db.PrefixedKey(authSourceByNameRedisKey(source.Name))).Result()
|
|
if err != nil {
|
|
t.Fatalf("Exists(by-name key) error = %v", err)
|
|
}
|
|
if byNameExists != 0 {
|
|
t.Fatalf("by-name redis key still exists after invalidation")
|
|
}
|
|
}
|
|
|
|
func TestAuthSourceInvalidationPubSubClearsPeerRAM(t *testing.T) {
|
|
dbConn, _, cleanup := setupAuthSourceCacheTest(t)
|
|
defer cleanup()
|
|
ctx := context.Background()
|
|
|
|
source := model.AuthSource{
|
|
Name: "pubsub-source",
|
|
Type: model.AuthSourceTypeOIDC,
|
|
DisplayName: "PubSub Source",
|
|
IsActive: true,
|
|
ClientID: "client-id",
|
|
ClientSecret: "client-secret",
|
|
OpenIDDiscoveryURL: "https://issuer.example.com",
|
|
}
|
|
if err := model.CreateAuthSource(ctx, &source); err != nil {
|
|
t.Fatalf("CreateAuthSource() error = %v", err)
|
|
}
|
|
|
|
if _, err := GetActiveAuthSourcesCached(ctx); err != nil {
|
|
t.Fatalf("GetActiveAuthSourcesCached() error = %v", err)
|
|
}
|
|
if err := dbConn.Delete(&model.AuthSource{}, "id = ?", source.ID).Error; err != nil {
|
|
t.Fatalf("Delete(auth source) error = %v", err)
|
|
}
|
|
if _, err := GetActiveAuthSourcesCached(ctx); err != nil {
|
|
t.Fatalf("expected RAM cache hit before pub/sub invalidation: %v", err)
|
|
}
|
|
|
|
if err := db.Redis.Publish(ctx, authSourceInvalidationChannel, "reset").Err(); err != nil {
|
|
t.Fatalf("publish invalidation: %v", err)
|
|
}
|
|
|
|
deadline := time.Now().Add(500 * time.Millisecond)
|
|
for time.Now().Before(deadline) {
|
|
if _, ok := authSourceActiveRAM.GetIfPresent(authSourceActiveRAMKey); !ok {
|
|
break
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
if _, ok := authSourceActiveRAM.GetIfPresent(authSourceActiveRAMKey); ok {
|
|
t.Fatal("expected peer RAM cache to be cleared by pub/sub")
|
|
}
|
|
} |