Files
jiang13-bbs/backend/config/config_test.go

307 lines
8.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func backendRoot(t *testing.T) string {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
root, err := filepath.Abs(filepath.Join(wd, ".."))
if err != nil {
t.Fatal(err)
}
return root
}
func assertPath(t *testing.T, got, want string) {
t.Helper()
g, err := filepath.Abs(got)
if err != nil {
t.Fatal(err)
}
w, err := filepath.Abs(want)
if err != nil {
t.Fatal(err)
}
if filepath.Clean(g) != filepath.Clean(w) {
t.Fatalf("路径不符:\n got %s\n want %s", g, w)
}
}
func TestResolveWorkPathFromOfficialCwd(t *testing.T) {
root := backendRoot(t)
t.Chdir(root)
got, err := resolveWorkPath()
if err != nil {
t.Fatal(err)
}
assertPath(t, got, root)
}
func TestResolveWorkPathFromCmdJiang13(t *testing.T) {
root := backendRoot(t)
t.Chdir(filepath.Join(root, "cmd", "jiang13"))
got, err := resolveWorkPath()
if err != nil {
t.Fatal(err)
}
assertPath(t, got, root)
}
func TestResolveWorkPathFromRepoRoot(t *testing.T) {
root := backendRoot(t)
t.Chdir(filepath.Join(root, ".."))
got, err := resolveWorkPath()
if err != nil {
t.Fatal(err)
}
assertPath(t, got, root)
}
func TestResolveWorkPathEnvOverride(t *testing.T) {
root := backendRoot(t)
t.Setenv("JIANG13_WORK_PATH", root)
t.Chdir(t.TempDir())
got, err := resolveWorkPath()
if err != nil {
t.Fatal(err)
}
assertPath(t, got, root)
}
func TestRejectStrayDataDir(t *testing.T) {
err := rejectStrayDataDir(`C:\proj\backend\cmd\jiang13\data`)
if err == nil {
t.Fatal("期望拒绝 cmd/jiang13/data")
}
if !strings.Contains(err.Error(), "拒绝使用数据目录") {
t.Fatalf("错误文案不符: %v", err)
}
if err := rejectStrayDataDir(`C:\proj\backend\data`); err != nil {
t.Fatal(err)
}
}
func TestAllowOriginDevAndProd(t *testing.T) {
dev := &Config{DevMode: true}
if !dev.AllowOrigin("http://localhost:3000") || !dev.AllowOrigin("http://127.0.0.1:3000") {
t.Fatal("开发态应放行 next dev Origin")
}
if dev.AllowOrigin("https://bbs.example.com") {
t.Fatal("开发态不应放行生产域名")
}
prod := &Config{
DevMode: false,
SiteURL: "https://bbs.example.com",
CORSOrigins: []string{"https://mirror.example.com"},
}
if !prod.AllowOrigin("https://bbs.example.com/") {
t.Fatal("生产态应放行 SITE_URL")
}
if !prod.AllowOrigin("https://mirror.example.com") {
t.Fatal("生产态应放行 CORS_ORIGINS")
}
if prod.AllowOrigin("http://localhost:3000") {
t.Fatal("生产态不应放行 localhost")
}
}
const testJWTSecret = "unit-test-jwt-secret-32chars-min!!" // 满足 ≥32,非生产用
func TestIsWeakJWTSecret(t *testing.T) {
if !isWeakJWTSecret("") || !isWeakJWTSecret("short") {
t.Fatal("空/过短应判弱")
}
if !isWeakJWTSecret("abcdefghijklmnopqrstuvwxyzABCDEF") {
t.Fatal("旧确定性密钥应判弱")
}
if !isWeakJWTSecret("change-me") {
t.Fatal("占位值应判弱")
}
if isWeakJWTSecret(testJWTSecret) {
t.Fatal("测试强密钥不应判弱")
}
}
func TestGenerateSecretNotDeterministic(t *testing.T) {
a, err := generateSecret(32)
if err != nil {
t.Fatal(err)
}
b, err := generateSecret(32)
if err != nil {
t.Fatal(err)
}
if a == b {
t.Fatal("连续两次生成不应相同")
}
if isWeakJWTSecret(a) {
t.Fatalf("生成结果被误判为弱: %q", a)
}
}
func TestParseRejectsWeakExplicitSecret(t *testing.T) {
work := t.TempDir()
if err := os.WriteFile(filepath.Join(work, "app.ini"), []byte("[security]\nJWT_SECRET = change-me\n"), 0600); err != nil {
t.Fatal(err)
}
t.Setenv("JIANG13_WORK_PATH", work)
t.Setenv("JWT_SECRET", "")
if _, err := Parse(); err == nil {
t.Fatal("显式弱密钥应拒绝启动")
}
}
func TestParseSettingsMasterKeyFromIni(t *testing.T) {
work := t.TempDir()
key := "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="
if err := os.WriteFile(filepath.Join(work, "app.ini"), []byte("[security]\nJWT_SECRET = "+testJWTSecret+"\nSETTINGS_MASTER_KEY = "+key+"\n"), 0600); err != nil {
t.Fatal(err)
}
t.Setenv("JIANG13_WORK_PATH", work)
t.Setenv("SETTINGS_MASTER_KEY", "")
t.Setenv("JWT_SECRET", "")
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()
t.Setenv("JIANG13_WORK_PATH", work)
t.Setenv("DEV_MODE", "false")
t.Setenv("SITE_URL", "https://bbs.example.com/")
t.Setenv("CORS_ORIGINS", " https://a.example.com ,https://b.example.com/ ")
t.Setenv("DATA_DIR", data)
t.Setenv("JWT_SECRET", testJWTSecret)
cfg, err := Parse()
if err != nil {
t.Fatal(err)
}
if cfg.DevMode {
t.Fatal("DEV_MODE=false 应关闭开发态")
}
if cfg.SiteURL != "https://bbs.example.com" {
t.Fatalf("SITE_URL 未去尾斜杠: %q", cfg.SiteURL)
}
if len(cfg.CORSOrigins) != 2 || cfg.CORSOrigins[0] != "https://a.example.com" || cfg.CORSOrigins[1] != "https://b.example.com" {
t.Fatalf("CORS_ORIGINS 解析错误: %#v", cfg.CORSOrigins)
}
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 = "+testJWTSecret+"\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)
}
}
}