完善站点运营设置:分路由管理、登录可见评论、邮件模板内联与关闭注册 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-23 00:17:00 +08:00
parent acde7ee288
commit 6f054a903c
98 changed files with 8239 additions and 2073 deletions

View File

@@ -114,6 +114,33 @@ func TestAllowOriginDevAndProd(t *testing.T) {
}
}
func TestParseSettingsMasterKeyFromIni(t *testing.T) {
work := t.TempDir()
key := "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="
if err := os.WriteFile(filepath.Join(work, "app.ini"), []byte("[security]\nJWT_SECRET = test-secret-not-for-prod\nSETTINGS_MASTER_KEY = "+key+"\n"), 0600); err != nil {
t.Fatal(err)
}
t.Setenv("JIANG13_WORK_PATH", work)
t.Setenv("SETTINGS_MASTER_KEY", "")
cfg, err := Parse()
if err != nil {
t.Fatal(err)
}
if cfg.SettingsMasterKey != key {
t.Fatalf("未从 app.ini 读取主密钥: %q", cfg.SettingsMasterKey)
}
override := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
t.Setenv("SETTINGS_MASTER_KEY", override)
cfg, err = Parse()
if err != nil {
t.Fatal(err)
}
if cfg.SettingsMasterKey != override {
t.Fatalf("环境变量应覆盖 app.ini: %q", cfg.SettingsMasterKey)
}
}
func TestParseSiteURLAndDataDir(t *testing.T) {
work := t.TempDir()
data := t.TempDir()
@@ -139,3 +166,94 @@ func TestParseSiteURLAndDataDir(t *testing.T) {
}
assertPath(t, cfg.DataDir, data)
}
func TestEnsureAppIniBackfillsMissingKeys(t *testing.T) {
work := t.TempDir()
iniPath := filepath.Join(work, "app.ini")
// 旧版精简配置:缺 SITE_URL / CORS_ORIGINS / SETTINGS_MASTER_KEY
old := "[server]\nHTTP_PORT = 3001\n\n[database]\nDSN = postgres://u:p@localhost/db\n\n[security]\nJWT_SECRET = keep-me\n\n[paths]\nDATA = data\n\n[app]\nDEV_MODE = true\n"
if err := os.WriteFile(iniPath, []byte(old), 0600); err != nil {
t.Fatal(err)
}
f, err := loadAndEnsureAppIni(iniPath)
if err != nil {
t.Fatal(err)
}
if !f.Section("app").HasKey("SITE_URL") || !f.Section("app").HasKey("CORS_ORIGINS") {
t.Fatal("应补全 SITE_URL / CORS_ORIGINS")
}
if got := f.Section("app").Key("SITE_URL").String(); got != "http://localhost:3000" {
t.Fatalf("开发态缺 SITE_URL 应补本机 origin,got %q", got)
}
if got := f.Section("security").Key("JWT_SECRET").String(); got != "keep-me" {
t.Fatalf("已有值被改写: %q", got)
}
if !f.Section("security").HasKey("SETTINGS_MASTER_KEY") {
t.Fatal("应补全 SETTINGS_MASTER_KEY 空键")
}
// 再生产态缺项:只留空键,不写 localhost
prod := t.TempDir()
prodIni := filepath.Join(prod, "app.ini")
if err := os.WriteFile(prodIni, []byte("[app]\nDEV_MODE = false\n"), 0600); err != nil {
t.Fatal(err)
}
pf, err := loadAndEnsureAppIni(prodIni)
if err != nil {
t.Fatal(err)
}
if got := pf.Section("app").Key("SITE_URL").String(); got != "" {
t.Fatalf("生产态缺 SITE_URL 应留空提示填写,got %q", got)
}
}
func TestParseSiteURLFromIni(t *testing.T) {
work := t.TempDir()
body := "[security]\nJWT_SECRET = test-secret-not-for-prod\n\n[app]\nDEV_MODE = true\nSITE_URL = https://forum.example.com/\nCORS_ORIGINS = https://a.example.com, https://b.example.com/\n"
if err := os.WriteFile(filepath.Join(work, "app.ini"), []byte(body), 0600); err != nil {
t.Fatal(err)
}
t.Setenv("JIANG13_WORK_PATH", work)
t.Setenv("SITE_URL", "")
t.Setenv("CORS_ORIGINS", "")
t.Setenv("DEV_MODE", "")
t.Setenv("JWT_SECRET", "")
cfg, err := Parse()
if err != nil {
t.Fatal(err)
}
if cfg.SiteURL != "https://forum.example.com" {
t.Fatalf("未从 app.ini 读取 SITE_URL: %q", cfg.SiteURL)
}
if len(cfg.CORSOrigins) != 2 || cfg.CORSOrigins[0] != "https://a.example.com" {
t.Fatalf("未从 app.ini 读取 CORS_ORIGINS: %#v", cfg.CORSOrigins)
}
t.Setenv("SITE_URL", "https://env.example.com")
cfg, err = Parse()
if err != nil {
t.Fatal(err)
}
if cfg.SiteURL != "https://env.example.com" {
t.Fatalf("环境变量应覆盖 app.ini SITE_URL: %q", cfg.SiteURL)
}
}
func TestEnsureAppIniCreatesWhenMissing(t *testing.T) {
work := t.TempDir()
iniPath := filepath.Join(work, "app.ini")
f, err := loadAndEnsureAppIni(iniPath)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(iniPath); err != nil {
t.Fatal("应创建 app.ini")
}
for _, spec := range appIniSchema {
if !f.Section(spec.section).HasKey(spec.key) {
t.Fatalf("新建文件缺少 %s.%s", spec.section, spec.key)
}
}
}