viper.Unmarshal 只解码 AllKeys() 返回的键,而 AllKeys() 只汇总 SetDefault、 配置文件和显式 BindEnv 三个来源。AutomaticEnv 仅能覆盖已在其中的键,无法引入 新键;能兜底的 viper_bind_struct 又被 build tag 排除(我们只用 -tags embed)。 因此任何「没有注册默认值、且不在 config.yaml 里」的配置项,其环境变量会被静默 丢弃。image_storage 的 endpoint/bucket/access_key_id/secret_access_key/ public_base_url 正属此列,于是纯环境变量部署落到最坏组合:IMAGE_STORAGE_ENABLED 生效使 Enabled=true,四个凭证却为空 → Active()=false → 异步生图接口整体 404, 运维看到的却是"凭证不完整"。deploy/docker-compose.yml 默认就是纯环境变量驱动, 且自动生成的 config.yaml 从不写 image_storage 段,必然踩中(见 #4458、#4542)。 同类缺口不止于此:github_oauth、google_oauth、dingtalk_connect 三组第三方登录 配置(含 client_secret)同样完全无法用环境变量设置。 - 为这些键注册零值默认,使其进入 AllKeys() 而可被环境变量覆盖。零值与"键缺失" 时的解码结果一致,故行为不变。 - sticky_escape_enabled 例外:它的实际默认是 true(靠 IsSet 守卫在解码后补上), 注册 false 会让 IsSet 恒真而永久关闭该特性,故直接注册 true。 - 启动告警补上 missing_keys 字段,指明到底哪个凭证为空。 - 新增反射守卫测试:Config 结构体上每个可由环境变量表达的字段都必须已注册默认值。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHreE5pzCkSYz7J45fmd2Y
42 lines
1.7 KiB
Go
42 lines
1.7 KiB
Go
//go:build unit
|
|
|
|
package config
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestLoadImageStorageFromEnv guards against a viper trap that silently disabled
|
|
// asynchronous image tasks for every environment-variable-only deployment.
|
|
//
|
|
// viper only decodes keys returned by AllKeys(), which unions SetDefault keys,
|
|
// config-file keys and explicit BindEnv keys. AutomaticEnv can override a key
|
|
// that is already in that list, but it never introduces a new one. Credentials
|
|
// such as image_storage.bucket therefore need an (empty) default registered, or
|
|
// IMAGE_STORAGE_BUCKET is dropped on the floor and Active() stays false while
|
|
// image_storage.enabled reads true — the endpoints 404 with no useful signal.
|
|
func TestLoadImageStorageFromEnv(t *testing.T) {
|
|
resetViperWithJWTSecret(t)
|
|
t.Setenv("IMAGE_STORAGE_ENABLED", "true")
|
|
t.Setenv("IMAGE_STORAGE_ENDPOINT", "https://acct.r2.cloudflarestorage.com")
|
|
t.Setenv("IMAGE_STORAGE_BUCKET", "my-images")
|
|
t.Setenv("IMAGE_STORAGE_ACCESS_KEY_ID", "ak")
|
|
t.Setenv("IMAGE_STORAGE_SECRET_ACCESS_KEY", "sk")
|
|
t.Setenv("IMAGE_STORAGE_PUBLIC_BASE_URL", "https://cdn.example.com")
|
|
|
|
cfg, err := Load()
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, cfg.ImageStorage.Enabled)
|
|
require.Equal(t, "https://acct.r2.cloudflarestorage.com", cfg.ImageStorage.Endpoint)
|
|
require.Equal(t, "my-images", cfg.ImageStorage.Bucket)
|
|
require.Equal(t, "ak", cfg.ImageStorage.AccessKeyID)
|
|
require.Equal(t, "sk", cfg.ImageStorage.SecretAccessKey)
|
|
require.Equal(t, "https://cdn.example.com", cfg.ImageStorage.PublicBaseURL)
|
|
|
|
require.True(t, cfg.ImageStorage.IsConfigured())
|
|
require.True(t, cfg.ImageStorage.Active(), "async image tasks must be active when every credential is supplied via env")
|
|
}
|