Files
sub2api/backend/internal/plugin/config_test.go
T
shaw 8ba2fbd786 feat(plugin): Phase-1/1.5 进程内插件内核 + 开发套件后端
Caddy 式进程内模块系统(详见 docs/plugin-architecture/MODULE-AUTHOR-GUIDE.md):
- internal/plugin:命名空间注册表(init 注册/重复 panic)、Provision→Validate→Start→Stop
  生命周期(Start 半途逆序回滚、Stop 逆序+ctx deadline)、Host 四项 ports 能力面、
  modules: 配置子树(enabled 三态;viper 含点 key 手工提取)、Runtime 状态机
- internal/plugin/plugintest:模块测试夹具(NewHost Options + RunLifecycle)
- internal/modules:standard/imports.go 唯一插装清单 + job.hello 示例模块(默认 disabled)
- Wire/main 接入:Build+Start 失败 fail-fast(显式 Cleanup 防 leader 锁泄漏)、
  runtime.Stop 在 cleanup 并行组首位(先于 Redis/Ent)
- admin 可观测:GET /api/v1/admin/modules(只读,错误文本脱敏)
- 默认配置零行为变更(Phase-0 安全网 + bench allocs 零漂移验证)

含两轮对抗式审计修复(1×P1 启动失败泄漏 leader 锁 + 11×P2)与回归测试。
2026-06-11 22:58:51 +08:00

136 lines
4.5 KiB
Go

package plugin
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestParseConfigEnabledTriState(t *testing.T) {
cfg, err := ParseConfig(map[string]map[string]any{
"job.on": {"enabled": true},
"job.off": {"enabled": false},
"job.unset": {"greeting": "hi"},
"job.strbool": {"enabled": "true"}, // 弱类型:字符串 bool(如来自环境变量)
})
require.NoError(t, err)
require.NotNil(t, cfg["job.on"].Enabled)
require.True(t, *cfg["job.on"].Enabled)
require.NotNil(t, cfg["job.off"].Enabled)
require.False(t, *cfg["job.off"].Enabled)
require.Nil(t, cfg["job.unset"].Enabled, "未显式配置 enabled 时必须为 nil(用模块默认值)")
require.NotNil(t, cfg["job.strbool"].Enabled)
require.True(t, *cfg["job.strbool"].Enabled)
}
func TestParseConfigSeparatesEnabledFromRaw(t *testing.T) {
cfg, err := ParseConfig(map[string]map[string]any{
"job.hello": {"enabled": true, "greeting": "hi", "interval": "5s"},
})
require.NoError(t, err)
mc := cfg["job.hello"]
require.Equal(t, map[string]any{"greeting": "hi", "interval": "5s"}, mc.Raw,
"enabled 键属于内核,不得混入模块私有配置")
}
func TestParseConfigErrors(t *testing.T) {
t.Run("invalid module ID", func(t *testing.T) {
_, err := ParseConfig(map[string]map[string]any{"Job.Hello": {}})
require.Error(t, err)
require.Contains(t, err.Error(), "Job.Hello")
})
t.Run("invalid enabled type", func(t *testing.T) {
_, err := ParseConfig(map[string]map[string]any{"job.hello": {"enabled": 123}})
require.Error(t, err)
require.Contains(t, err.Error(), "job.hello")
})
t.Run("invalid enabled string", func(t *testing.T) {
_, err := ParseConfig(map[string]map[string]any{"job.hello": {"enabled": "not-a-bool"}})
require.Error(t, err)
})
t.Run("unregistered but valid ID is allowed", func(t *testing.T) {
cfg, err := ParseConfig(map[string]map[string]any{"future.module": {"enabled": true}})
require.NoError(t, err)
require.Contains(t, cfg, ModuleID("future.module"))
})
}
type helloModuleConfig struct {
Greeting string `mapstructure:"greeting"`
Interval time.Duration `mapstructure:"interval"`
Targets []string `mapstructure:"targets"`
Limit int `mapstructure:"limit"`
}
func TestConfigOfDecodesRawIntoStruct(t *testing.T) {
cfg, err := ParseConfig(map[string]map[string]any{
"job.hello": {
"enabled": true,
"greeting": "hi",
"interval": "5s", // 字符串 → time.Duration(与 viper 默认钩子一致)
"targets": "a,b,c", // 逗号分隔 → 切片(与 viper 默认钩子一致)
"limit": "42", // 弱类型:字符串 → int
},
})
require.NoError(t, err)
var out helloModuleConfig
require.NoError(t, cfg.Of("job.hello", &out))
require.Equal(t, "hi", out.Greeting)
require.Equal(t, 5*time.Second, out.Interval)
require.Equal(t, []string{"a", "b", "c"}, out.Targets)
require.Equal(t, 42, out.Limit)
}
func TestConfigOfMissingModuleLeavesOutUntouched(t *testing.T) {
cfg := Config{}
out := helloModuleConfig{Greeting: "default-value"}
require.NoError(t, cfg.Of("job.absent", &out))
require.Equal(t, "default-value", out.Greeting, "模块未配置时不得修改 out(保留模块默认值)")
}
func TestConfigOfTypeErrorContainsModuleID(t *testing.T) {
cfg, err := ParseConfig(map[string]map[string]any{
"job.hello": {"limit": "not-an-int"},
})
require.NoError(t, err)
var out helloModuleConfig
err = cfg.Of("job.hello", &out)
require.Error(t, err)
require.Contains(t, err.Error(), "job.hello", "解码错误必须包含模块 ID")
}
func TestConfigEnabledFor(t *testing.T) {
on, off := true, false
cfg := Config{
"job.explicit-on": {Enabled: &on},
"job.explicit-off": {Enabled: &off},
"job.mentioned": {},
}
tests := []struct {
name string
info ModuleInfo
want bool
}{
{"explicit on overrides default off", ModuleInfo{ID: "job.explicit-on", EnabledByDefault: false}, true},
{"explicit off overrides default on", ModuleInfo{ID: "job.explicit-off", EnabledByDefault: true}, false},
{"mentioned without enabled uses default true", ModuleInfo{ID: "job.mentioned", EnabledByDefault: true}, true},
{"mentioned without enabled uses default false", ModuleInfo{ID: "job.mentioned", EnabledByDefault: false}, false},
{"absent uses default true", ModuleInfo{ID: "job.absent", EnabledByDefault: true}, true},
{"absent uses default false", ModuleInfo{ID: "job.absent", EnabledByDefault: false}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, cfg.enabledFor(tt.info))
})
}
}