- users 加 token_version(schema v4):改密/登出递增,该用户全部已发 access/refresh 立即作废;refresh 改轮换式换发新 cookie - 邀请码改精确匹配 + 注册成功后消费(一码一用),修复子串匹配让任意后缀放行、可无限薅体验金 - 生产校验拦下 .env.example 占位密钥;OT_ENV 枚举校验(拼错不再静默跳过断言);生产强制 COOKIE_SECURE;非 development 拒绝默认密码播种管理员 - WebAuthn 会话启用过期校验 + 4096 容量上限(未认证 login/begin 可无限撑爆内存),begin/complete 挂登录级限速 - /api/v1 挂 1MB 请求体上限;login 先限 IP 桶再解析 body;全局安全响应头 + CSP - 连接池配置(PG 30/SQLite 8);SQLite DSN 自动追加 WAL + busy_timeout(驱动只认 _pragma,旧注释的 _journal_mode 写法不生效)
88 lines
3.0 KiB
Go
88 lines
3.0 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func prodCfg() *Config {
|
|
c := &Config{Env: "production"}
|
|
c.JWT.Secret = "a-real-random-secret-not-the-default"
|
|
c.JWT.CookieSecure = true
|
|
c.Master = "0123456789abcdef0123456789abcdef"
|
|
c.AdminPassword = "strong-enough-password"
|
|
return c
|
|
}
|
|
|
|
func TestValidateProduction(t *testing.T) {
|
|
if err := prodCfg().validate(); err != nil {
|
|
t.Fatalf("配齐密钥应通过:%v", err)
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*Config)
|
|
want string
|
|
}{
|
|
{"默认 jwt secret", func(c *Config) { c.JWT.Secret = "dev-only-secret-change-me" }, "OT_JWT_SECRET"},
|
|
{"空 jwt secret", func(c *Config) { c.JWT.Secret = "" }, "OT_JWT_SECRET"},
|
|
// 复制 .env.example 不改密钥是最常见的部署路径,占位值必须拦下
|
|
{"示例文件里的 jwt secret 占位值", func(c *Config) { c.JWT.Secret = "change-me-to-a-long-random-string" }, "OT_JWT_SECRET"},
|
|
{"缺失 master key", func(c *Config) { c.Master = "" }, "OT_MASTER_KEY"},
|
|
{"过短 master key", func(c *Config) { c.Master = "short" }, "OT_MASTER_KEY"},
|
|
{"示例文件里的 master key 占位值", func(c *Config) { c.Master = "change-me-master-key" }, "OT_MASTER_KEY"},
|
|
{"默认管理员密码", func(c *Config) { c.AdminPassword = "admin123" }, "OT_ADMIN_PASSWORD"},
|
|
{"未设管理员密码", func(c *Config) { c.AdminPassword = "" }, "OT_ADMIN_PASSWORD"},
|
|
{"cookie 未开 secure", func(c *Config) { c.JWT.CookieSecure = false }, "OT_JWT_COOKIE_SECURE"},
|
|
}
|
|
for _, tc := range cases {
|
|
c := prodCfg()
|
|
tc.mutate(c)
|
|
err := c.validate()
|
|
if err == nil {
|
|
t.Fatalf("%s:应被拒绝", tc.name)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("%s:报错应点名 %s,实际 %v", tc.name, tc.want, err)
|
|
}
|
|
}
|
|
|
|
// 多项同时缺失要一次全说清,别让人改一个试一个
|
|
c := prodCfg()
|
|
c.JWT.Secret = ""
|
|
c.Master = ""
|
|
err := c.validate()
|
|
if err == nil || !strings.Contains(err.Error(), "OT_JWT_SECRET") || !strings.Contains(err.Error(), "OT_MASTER_KEY") {
|
|
t.Fatalf("应一次列出全部问题:%v", err)
|
|
}
|
|
|
|
// 开发环境不拦(本地跑默认值就是设计如此)
|
|
dev := prodCfg()
|
|
dev.Env = "development"
|
|
dev.JWT.Secret = "dev-only-secret-change-me"
|
|
dev.JWT.CookieSecure = false
|
|
dev.Master = ""
|
|
dev.AdminPassword = "admin123"
|
|
if err := dev.validate(); err != nil {
|
|
t.Fatalf("development 不应拦截:%v", err)
|
|
}
|
|
|
|
// env 拼错(如 "prod")会静默跳过全部生产断言,必须枚举校验直接拒绝
|
|
if err := (&Config{Env: "prod"}).validate(); err == nil || !strings.Contains(err.Error(), "OT_ENV") {
|
|
t.Fatalf("未知 env 值应被拒绝:%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSplitList(t *testing.T) {
|
|
if got := splitList(""); got != nil {
|
|
t.Fatalf("空串应为 nil,实际 %v", got)
|
|
}
|
|
if got := splitList(" , ,, "); got != nil {
|
|
t.Fatalf("纯分隔符应为 nil,实际 %v", got)
|
|
}
|
|
got := splitList("https://a.com, https://b.com ,,http://c.com")
|
|
if len(got) != 3 || got[0] != "https://a.com" || got[2] != "http://c.com" {
|
|
t.Fatalf("解析错误:%v", got)
|
|
}
|
|
}
|