154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package settings
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// TestEveryDeclaredKeyHasConsumer:每个声明出来的键都要有「谁在读」的说明,
|
|||
|
|
// 并且不能只是复述键名本身。这条测试防的是「界面能改、后端没人读」的黑洞设置项
|
|||
|
|
// (theme / timezone 曾经就是这样:常量、默认值、白名单、前端表单四处齐全,唯独没人消费)。
|
|||
|
|
func TestEveryDeclaredKeyHasConsumer(t *testing.T) {
|
|||
|
|
for _, spec := range Specs {
|
|||
|
|
if spec.Key == "" {
|
|||
|
|
t.Fatal("存在未填 Key 的 Spec")
|
|||
|
|
}
|
|||
|
|
if spec.Kind == "" {
|
|||
|
|
t.Fatalf("%s 未填 Kind", spec.Key)
|
|||
|
|
}
|
|||
|
|
if spec.Consumer == "" {
|
|||
|
|
t.Fatalf("%s 未填 Consumer(说明谁在读这个设置项)", spec.Key)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestNoDuplicateKeys:同一个键不能声明两次,否则 Lookup / Defaults 会悄悄取其中一个。
|
|||
|
|
func TestNoDuplicateKeys(t *testing.T) {
|
|||
|
|
seen := map[string]bool{}
|
|||
|
|
for _, spec := range Specs {
|
|||
|
|
if seen[spec.Key] {
|
|||
|
|
t.Fatalf("重复声明的设置项:%s", spec.Key)
|
|||
|
|
}
|
|||
|
|
seen[spec.Key] = true
|
|||
|
|
}
|
|||
|
|
if len(seen) != len(Specs) {
|
|||
|
|
t.Fatalf("Specs 数量 %d 与去重后 %d 不一致", len(Specs), len(seen))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestKindableDefaultsMatchKind:默认值必须能被它声明的 Kind 解析,
|
|||
|
|
// 否则 GetInt/GetBool 在没写过值时会拿到脏数据。
|
|||
|
|
func TestKindableDefaultsMatchKind(t *testing.T) {
|
|||
|
|
for _, spec := range Specs {
|
|||
|
|
if spec.Default == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
switch spec.Kind {
|
|||
|
|
case KindInt:
|
|||
|
|
if _, ok := parseInt(spec.Default); !ok {
|
|||
|
|
t.Fatalf("%s 的默认值 %q 不是合法整数", spec.Key, spec.Default)
|
|||
|
|
}
|
|||
|
|
case KindBool:
|
|||
|
|
if _, ok := parseBool(spec.Default); !ok {
|
|||
|
|
t.Fatalf("%s 的默认值 %q 不是合法布尔值", spec.Key, spec.Default)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestDefaultsDerivedFromSpecs:Defaults 必须与 Specs 一致,防止两处各写一份。
|
|||
|
|
func TestDefaultsDerivedFromSpecs(t *testing.T) {
|
|||
|
|
index := map[string]string{}
|
|||
|
|
for _, spec := range Specs {
|
|||
|
|
if spec.Default != "" {
|
|||
|
|
index[spec.Key] = spec.Default
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if len(index) != len(Defaults) {
|
|||
|
|
t.Fatalf("Defaults 数量 %d 与 Specs 中带默认值的条目 %d 不一致", len(Defaults), len(index))
|
|||
|
|
}
|
|||
|
|
for key, want := range index {
|
|||
|
|
if got := Defaults[key]; got != want {
|
|||
|
|
t.Fatalf("%s 默认值不符:got %q want %q", key, got, want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestInternalStateKeysNotWritable:程序自己维护的状态不能通过 API 写入,
|
|||
|
|
// 否则用户可以把 frp_resume_on_start 之类改成任意值,破坏重启恢复逻辑。
|
|||
|
|
func TestInternalStateKeysNotWritable(t *testing.T) {
|
|||
|
|
internal := []string{
|
|||
|
|
KeyFrpResumeOnStart, KeyStatusPageCodeHash,
|
|||
|
|
KeyChinaCIDRUpdatedAt, KeyChinaCIDRCountV4, KeyChinaCIDRCountV6, KeyChinaCIDRLastError,
|
|||
|
|
KeyNginxGlobalMode,
|
|||
|
|
}
|
|||
|
|
writable := WritableKeys()
|
|||
|
|
for _, key := range internal {
|
|||
|
|
if writable[key] {
|
|||
|
|
t.Fatalf("%s 是内部状态键,不应出现在可写白名单里", key)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestSecretKeysAreDeclared:已知的敏感键必须被标记为 Secret,读接口才会打码。
|
|||
|
|
func TestSecretKeysAreDeclared(t *testing.T) {
|
|||
|
|
secrets := []string{
|
|||
|
|
KeyNotifySMTPPassword, KeyNotifyWebhookSecret, KeyNotifyTelegramToken,
|
|||
|
|
KeyZeroSSLAPIKey, KeyLiteSSLEABHMAC, KeyStatusPageCodeHash,
|
|||
|
|
}
|
|||
|
|
for _, key := range secrets {
|
|||
|
|
spec, ok := Lookup(key)
|
|||
|
|
if !ok {
|
|||
|
|
t.Fatalf("%s 未在注册表里声明", key)
|
|||
|
|
}
|
|||
|
|
if !spec.Secret {
|
|||
|
|
t.Fatalf("%s 应标记为 Secret", key)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestWritableWhitelistContainsCoreKeys:核心可配置项必须在白名单里,
|
|||
|
|
// 否则前端保存会静默丢字段(PUT 会按白名单过滤)。
|
|||
|
|
func TestWritableWhitelistContainsCoreKeys(t *testing.T) {
|
|||
|
|
writable := WritableKeys()
|
|||
|
|
core := []string{
|
|||
|
|
KeyTheme, KeyTimezone, KeyLogRetentionDays,
|
|||
|
|
KeyDDNSCheckInterval, KeyCertRenewThreshold,
|
|||
|
|
KeyBackupAutoEnabled, KeyBackupKeepCount,
|
|||
|
|
KeyStatusPageEnabled, KeyTerminalEnabled, KeyTerminalRecording,
|
|||
|
|
}
|
|||
|
|
for _, key := range core {
|
|||
|
|
if !writable[key] {
|
|||
|
|
t.Fatalf("%s 应允许 API 写入", key)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// parseInt / parseBool 是注册表测试用的宽松解析,与 Store 的行为保持一致。
|
|||
|
|
func parseInt(raw string) (int, bool) {
|
|||
|
|
n := 0
|
|||
|
|
neg := false
|
|||
|
|
for i, c := range raw {
|
|||
|
|
if i == 0 && c == '-' {
|
|||
|
|
neg = true
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if c < '0' || c > '9' {
|
|||
|
|
return 0, false
|
|||
|
|
}
|
|||
|
|
n = n*10 + int(c-'0')
|
|||
|
|
}
|
|||
|
|
if neg {
|
|||
|
|
n = -n
|
|||
|
|
}
|
|||
|
|
return n, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func parseBool(raw string) (bool, bool) {
|
|||
|
|
switch raw {
|
|||
|
|
case "1", "true", "yes", "on", "0", "false", "no", "off":
|
|||
|
|
return raw == "1" || raw == "true" || raw == "yes" || raw == "on", true
|
|||
|
|
default:
|
|||
|
|
return false, false
|
|||
|
|
}
|
|||
|
|
}
|