package systemconfig_test import ( "context" stderrors "errors" "sync" "testing" "time" "github.com/break/junhong_cmp_fiber/internal/infrastructure/systemconfig" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/testutil" "github.com/break/junhong_cmp_fiber/pkg/constants" ) type unavailableCache struct{} func (unavailableCache) Get(context.Context, string) (string, error) { return "", stderrors.New("测试缓存不可用") } func (unavailableCache) Set(context.Context, string, string, time.Duration) error { return stderrors.New("测试缓存不可用") } func (unavailableCache) Delete(context.Context, string) error { return stderrors.New("测试缓存不可用") } type alertRecorder struct { mu sync.Mutex codes []string } func (r *alertRecorder) Warn(_ context.Context, code, _, _, _ string) { r.mu.Lock() defer r.mu.Unlock() r.codes = append(r.codes, code) } func TestReaderUsesRedisHitAndFallsBackToPostgresOnMiss(t *testing.T) { db := testutil.NewPostgresTransaction(t) redisClient := testutil.NewRedisClient(t) testutil.CreateTemporarySystemConfigTable(t, db) registry := systemconfig.NewRegistry() definition := systemconfig.Definition{ Key: "foundation.reader.limit", Module: "foundation", ValueType: constants.SystemConfigTypeInt, DefaultValue: "10", Description: "读取器测试上限", } if err := registry.Register(definition); err != nil { t.Fatalf("注册系统配置失败:%v", err) } if err := db.Create(&model.SystemConfig{ ConfigKey: definition.Key, ConfigValue: "20", ValueType: definition.ValueType, Module: definition.Module, Description: definition.Description, }).Error; err != nil { t.Fatalf("准备数据库配置失败:%v", err) } cacheKey := constants.RedisSystemConfigKey(definition.Key) t.Cleanup(func() { _ = redisClient.Del(context.Background(), cacheKey).Err() }) if err := redisClient.Set(context.Background(), cacheKey, "30", constants.SystemConfigCacheTTL).Err(); err != nil { t.Fatalf("准备 Redis 缓存失败:%v", err) } reader := systemconfig.NewReader(db, registry, systemconfig.NewRedisCache(redisClient), nil) value, err := reader.Get(context.Background(), definition.Key) if err != nil || value != "30" { t.Fatalf("Redis 命中结果错误:值=%q,错误=%v", value, err) } if err := redisClient.Del(context.Background(), cacheKey).Err(); err != nil { t.Fatalf("清理 Redis 缓存失败:%v", err) } value, err = reader.Get(context.Background(), definition.Key) if err != nil || value != "20" { t.Fatalf("Redis 未命中时未回退 PostgreSQL:值=%q,错误=%v", value, err) } cached, err := redisClient.Get(context.Background(), cacheKey).Result() if err != nil || cached != "20" { t.Fatalf("PostgreSQL 结果未回填 Redis:值=%q,错误=%v", cached, err) } ttl, err := redisClient.TTL(context.Background(), cacheKey).Result() if err != nil || ttl <= 0 || ttl > constants.SystemConfigCacheTTL { t.Fatalf("Redis 回填 TTL 不符合公共常量:TTL=%s,错误=%v", ttl, err) } } func TestReaderFallsBackToPostgresWhenCacheIsUnavailable(t *testing.T) { db := testutil.NewPostgresTransaction(t) testutil.CreateTemporarySystemConfigTable(t, db) registry := systemconfig.NewRegistry() definition := systemconfig.Definition{ Key: "foundation.reader.fallback", Module: "foundation", ValueType: constants.SystemConfigTypeString, DefaultValue: "default", Description: "缓存故障回退测试值", } if err := registry.Register(definition); err != nil { t.Fatalf("注册系统配置失败:%v", err) } if err := db.Create(&model.SystemConfig{ ConfigKey: definition.Key, ConfigValue: "database", ValueType: definition.ValueType, Module: definition.Module, Description: definition.Description, }).Error; err != nil { t.Fatalf("准备数据库配置失败:%v", err) } alerts := &alertRecorder{} reader := systemconfig.NewReader(db, registry, unavailableCache{}, alerts) value, err := reader.Get(context.Background(), definition.Key) if err != nil || value != "database" { t.Fatalf("缓存不可用时未回退 PostgreSQL:值=%q,错误=%v", value, err) } alerts.mu.Lock() defer alerts.mu.Unlock() if len(alerts.codes) < 2 || alerts.codes[0] != "SYSTEM_CONFIG_CACHE_READ_FAILED" { t.Fatalf("缓存读写故障未产生安全告警:%v", alerts.codes) } }