Files
杭州明婳科技 96ce883d41 feat: cron/storage/cache/ratelimit 实例化 + trace 纳入 App 生命周期 (v1.4.0)
完成 instance+facade 一致性收尾,闭合三个原始问题:
- cron/storage/cache/ratelimit 实例化(App 持 per-App 实例 + SwapDefault* 提升全局默认
  + 包级 facade 代理,照 database.Manager/jwt.Manager 模式),消除单进程多 App 互相污染。
- trace 纳入 App 生命周期(WithTrace + doInit trace.Init + doShutdown trace.Close +
  config.TraceConfig + 自动装 Middleware)。不做实例隔离(OTel 进程全局)。
- 删除 WithoutWire 空函数技术债。

附带修复:
- cache/ratelimit redis 注入(jwt 模型:注入优先、database.GetRedis() 兜底)。
- commitReplacedResources 不再关闭 previousDB/previousRedis/loggerSnapshot
  (多 App 下 previous 可能是另一 App 的活跃资源,关闭致 redis: client is closed)。
- app.RateLimitRegistry() + App-bound 限流中间件(多 App per-App 计数隔离)。
- StorageManager.Close() 闭合 Init/Close 对称性(io.Closer 断言,预留扩展点),
  closeResources 与 rollbackReplacedResources 对称收口。

新增公开 API:WithTrace / WithCronTask;app.Scheduler()/Cache()/RedisClient()/
RateLimitRegistry();cron/storage/cache/middleware 各 SwapDefault*;config.TraceConfig;
middleware.RateLimitRegistry + App-bound 方法;middleware.WithRedisClient;
storage.StorageManager.Close。

Breaking:删 WithoutWire;cron.AddTask pre-Init 注册失效(改用 WithCronTask);
middleware.StopRateLimiters 语义收窄;commitReplacedResources 不再关 previous;
cache.NewRedisCacheWithRedis / CacheManager.InitWithRedis 等签名变更。
详见 CHANGELOG [1.4.0] 升级说明。

验证:go build + go vet + go test -race ./... 全绿(-buildvcs=false)。
独立对抗性复审无 CRITICAL/HIGH。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:25:37 +08:00

82 lines
2.1 KiB
Go

package config_test
import (
"strings"
"testing"
"github.com/EthanCodeCraft/xlgo-core/config"
)
func validBase() *config.Config {
return &config.Config{
Server: config.ServerConfig{Port: 8080},
}
}
func TestValidateOK(t *testing.T) {
if err := validBase().Validate(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidateServerPort(t *testing.T) {
c := validBase()
c.Server.Port = 99999
if err := c.Validate(); err == nil || !strings.Contains(err.Error(), "server.port") {
t.Fatalf("expected server.port error, got %v", err)
}
}
func TestValidateJWTSecretTooShort(t *testing.T) {
c := validBase()
c.JWT.Secret = "short"
if err := c.Validate(); err == nil || !strings.Contains(err.Error(), "jwt.secret") {
t.Fatalf("expected jwt.secret error, got %v", err)
}
}
func TestValidateJWTSecretAbsentSkipped(t *testing.T) {
c := validBase()
// secret 为空时不校验(未启用 jwt)
if err := c.Validate(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestValidateTraceSampleRatioWhenEnabled(t *testing.T) {
c := validBase()
c.Trace.Enabled = true
c.Trace.SampleRatio = 1.5
if err := c.Validate(); err == nil || !strings.Contains(err.Error(), "trace.sample_ratio") {
t.Fatalf("expected trace.sample_ratio error, got %v", err)
}
}
func TestValidateTraceDisabledSkipsRatio(t *testing.T) {
c := validBase()
// Enabled=false 时即使 SampleRatio 非法也不校验(trace 未启用)
c.Trace.SampleRatio = 1.5
if err := c.Validate(); err != nil {
t.Fatalf("unexpected error when trace disabled: %v", err)
}
}
func TestValidateDatabaseMissingHost(t *testing.T) {
c := validBase()
c.Database.Driver = "mysql"
c.Database.Port = 3306
c.Database.Name = "db"
// Host 留空
if err := c.Validate(); err == nil || !strings.Contains(err.Error(), "database.host") {
t.Fatalf("expected database.host error, got %v", err)
}
}
func TestValidateTLSMissingCert(t *testing.T) {
c := validBase()
c.Server.TLS.Enabled = true
if err := c.Validate(); err == nil || !strings.Contains(err.Error(), "cert_file") {
t.Fatalf("expected tls cert_file error, got %v", err)
}
}