Files

345 lines
10 KiB
Go
Raw Permalink Normal View History

2026-08-09 11:49:13 +08:00
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
2026-08-10 01:22:22 +08:00
func TestLoadDatabaseFromEnv_默认只连接本机3307且密码不泄露(t *testing.T) {
t.Setenv(databaseHostEnv, "")
t.Setenv(databasePortEnv, "")
t.Setenv(databaseNameEnv, "cmautobuy_test")
t.Setenv(databaseUserEnv, "cmautobuy_test")
t.Setenv(databasePasswordEnv, "secret-do-not-print")
cfg, err := LoadDatabaseFromEnv()
if err != nil {
t.Fatal(err)
}
if cfg.Host != "127.0.0.1" || cfg.Port != "3307" {
t.Fatalf("默认地址应为线上同机 MySQL 8,实际 %s:%s", cfg.Host, cfg.Port)
}
if got := cfg.String(); strings.Contains(got, cfg.Password) || !strings.Contains(got, "****") {
t.Fatalf("配置字符串泄露密码或没有打码:%s", got)
}
}
func TestLoadDatabaseFromEnv_缺少必填项时只报变量名(t *testing.T) {
t.Setenv(databaseNameEnv, "")
t.Setenv(databaseUserEnv, "")
t.Setenv(databasePasswordEnv, "")
_, err := LoadDatabaseFromEnv()
if err == nil || !strings.Contains(err.Error(), databasePasswordEnv) {
t.Fatalf("应明确列出缺失变量,实际 %v", err)
}
}
func TestLoadDatabaseFromPath_可以只用YAML配置(t *testing.T) {
clearDatabaseEnv(t)
path := writeConfigFile(t, `
database:
host: 127.0.0.1
port: "13307"
name: autobuy
user: buy
password: "yaml-secret"
tls_mode: verify_ca
tls_ca: certs/mysql84-ca.pem
`)
cfg, err := loadDatabaseFromPath(path)
if err != nil {
t.Fatal(err)
}
if cfg.Host != "127.0.0.1" || cfg.Port != "13307" || cfg.Name != "autobuy" || cfg.User != "buy" {
t.Fatalf("YAML 数据库配置读取错误:%s", cfg)
}
if cfg.Password != "yaml-secret" {
t.Fatal("YAML 数据库密码没有原样读出")
}
if cfg.TLSMode != DatabaseTLSVerifyCA || cfg.TLSCA != "certs/mysql84-ca.pem" {
t.Fatalf("YAML TLS 配置读取错误:%s", cfg)
}
}
func TestLoadDatabaseFromPath_环境变量按字段覆盖YAML(t *testing.T) {
clearDatabaseEnv(t)
t.Setenv(databasePortEnv, "23307")
t.Setenv(databaseUserEnv, "env-user")
t.Setenv(databasePasswordEnv, "env-secret")
path := writeConfigFile(t, `
database:
host: 127.0.0.1
port: "13307"
name: autobuy
user: yaml-user
password: "yaml-secret"
`)
cfg, err := loadDatabaseFromPath(path)
if err != nil {
t.Fatal(err)
}
if cfg.Host != "127.0.0.1" || cfg.Name != "autobuy" {
t.Fatalf("未覆盖字段应该保留 YAML 值:%s", cfg)
}
if cfg.Port != "23307" || cfg.User != "env-user" || cfg.Password != "env-secret" {
t.Fatalf("环境变量没有按字段覆盖 YAML:%s", cfg)
}
if strings.Contains(cfg.String(), "env-secret") || strings.Contains(cfg.String(), "yaml-secret") {
t.Fatalf("配置字符串泄露密码:%s", cfg)
}
}
func TestLoadDatabaseFromPath_文件不存在时完整环境变量仍可启动(t *testing.T) {
clearDatabaseEnv(t)
t.Setenv(databaseNameEnv, "autobuy")
t.Setenv(databaseUserEnv, "buy")
t.Setenv(databasePasswordEnv, "env-secret")
cfg, err := loadDatabaseFromPath(filepath.Join(t.TempDir(), "missing.yaml"))
if err != nil {
t.Fatal(err)
}
if cfg.Host != "127.0.0.1" || cfg.Port != "3307" {
t.Fatalf("线上环境变量模式应该保留安全默认地址,实际:%s", cfg)
}
if cfg.TLSMode != DatabaseTLSDisabled {
t.Fatalf("未配置 TLS 时应该保持本机连接兼容,实际:%s", cfg)
}
}
func TestLoadDatabaseFromPath_TLS环境变量覆盖YAML(t *testing.T) {
clearDatabaseEnv(t)
t.Setenv(databaseTLSModeEnv, DatabaseTLSVerifyCA)
t.Setenv(databaseTLSCAEnv, `D:\certs\mysql-ca.pem`)
path := writeConfigFile(t, `
database:
name: autobuy
user: buy
password: "yaml-secret"
tls_mode: disabled
`)
cfg, err := loadDatabaseFromPath(path)
if err != nil {
t.Fatal(err)
}
if cfg.TLSMode != DatabaseTLSVerifyCA || cfg.TLSCA != `D:\certs\mysql-ca.pem` {
t.Fatalf("TLS 环境变量没有覆盖 YAML:%s", cfg)
}
}
func TestLoadDatabaseFromPath_TLS模式校验(t *testing.T) {
clearDatabaseEnv(t)
path := writeConfigFile(t, `
database:
name: autobuy
user: buy
password: "yaml-secret"
tls_mode: preferred
`)
_, err := loadDatabaseFromPath(path)
if err == nil || !strings.Contains(err.Error(), "只能是 disabled 或 verify_ca") {
t.Fatalf("应该拒绝可能回退明文的 TLS 模式,实际:%v", err)
}
}
func TestLoadDatabaseFromPath_VerifyCA必须配置证书(t *testing.T) {
clearDatabaseEnv(t)
path := writeConfigFile(t, `
database:
name: autobuy
user: buy
password: "yaml-secret"
tls_mode: verify_ca
`)
_, err := loadDatabaseFromPath(path)
if err == nil || !strings.Contains(err.Error(), databaseTLSCAEnv) {
t.Fatalf("verify_ca 缺少 CA 时应该明确报错,实际:%v", err)
}
}
func clearDatabaseEnv(t *testing.T) {
t.Helper()
for _, name := range []string{
databaseHostEnv,
databasePortEnv,
databaseNameEnv,
databaseUserEnv,
databasePasswordEnv,
databaseTLSModeEnv,
databaseTLSCAEnv,
} {
t.Setenv(name, "")
}
}
func writeConfigFile(t *testing.T, yamlText string) string {
t.Helper()
path := filepath.Join(t.TempDir(), configFileName)
if err := os.WriteFile(path, []byte(yamlText), 0o600); err != nil {
t.Fatalf("写测试配置文件失败: %v", err)
}
return path
}
2026-08-09 11:49:13 +08:00
// loadFrom 是测试专用的小工具:把一段 YAML 文本写到临时目录里的
// config.yaml,绕开 ConfigPath()(它依赖 os.Executable,测试环境里
// 不可控),直接测 Load 里"读文件 + 解析"这段逻辑。
func loadFrom(t *testing.T, yamlText string) (*Config, error) {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, configFileName)
if yamlText != "" {
if err := os.WriteFile(path, []byte(yamlText), 0o644); err != nil {
t.Fatalf("写测试配置文件失败: %v", err)
}
}
raw, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, missingConfigError(path)
}
return nil, err
}
return parseConfig(raw, path)
}
func TestLoad_缺文件时提示复制模板(t *testing.T) {
// 注意:不要在错误信息断言里用测试名本身会出现的中文片段做 Contains 判断——
// t.TempDir() 生成的目录名会把测试函数名拼进路径,路径又被拼进错误信息,
// 断言字符串一旦和测试名撞了就会产生误报,这里刻意避开这个坑。
dir := t.TempDir()
missingPath := filepath.Join(dir, configFileName)
err := func() error {
_, e := os.ReadFile(missingPath)
if os.IsNotExist(e) {
return missingConfigError(missingPath)
}
return e
}()
if err == nil {
t.Fatal("缺文件时应该返回错误")
}
if !strings.Contains(err.Error(), "config.example.yaml") {
t.Errorf("错误信息应该提示复制 config.example.yaml,实际: %v", err)
}
if !strings.Contains(err.Error(), "复制") {
t.Errorf("错误信息应该给出「复制模板文件」这个具体操作,不是一句笼统的报错,实际: %v", err)
}
}
func TestLoad_纯数字密码加引号能读出字符串(t *testing.T) {
cfg, err := loadFrom(t, `
syb:
base_url: https://www.shunyunbaoerp.com
username: tester
password: "0012345"
page_size: 20
max_matches: 500
sync_from: "2026-07-01"
`)
if err != nil {
t.Fatalf("解析失败: %v", err)
}
if cfg.Syb.Password != "0012345" {
t.Errorf("密码应该原样保留字符串(含前导 0),实际 %q", cfg.Syb.Password)
}
if cfg.Syb.Username != "tester" {
t.Errorf("username 解析错误: %q", cfg.Syb.Username)
}
if cfg.Syb.PageSize != 20 || cfg.Syb.MaxMatches != 500 {
t.Errorf("数字字段解析错误: page_size=%d max_matches=%d", cfg.Syb.PageSize, cfg.Syb.MaxMatches)
}
}
func TestLoad_纯数字密码不加引号仍能读出字符串(t *testing.T) {
// go-yaml 对 "password: 0012345"(不加引号)这种写法的行为要在这里锁定:
// 如果被解析成整数再转字符串,前导 0 会丢,密码就错了。
// 这里不强制要求这种写法本身合法,只要求:如果它没有报错,
// 结果不能悄悄丢字符。
cfg, err := loadFrom(t, `
syb:
base_url: https://www.shunyunbaoerp.com
username: tester
password: 12345
`)
if err != nil {
// 反序列化到 string 字段直接报错也是可以接受的行为
// (文档 08 §8 描述的正是这种失败模式),不是本测试要断言的重点。
t.Skipf("不加引号的纯数字密码解析失败(符合文档描述的已知坑): %v", err)
}
if cfg.Syb.Password != "12345" {
t.Errorf("密码字符串不应该丢字符,实际 %q", cfg.Syb.Password)
}
}
func TestSybConfig_String不泄露密码(t *testing.T) {
cfg := SybConfig{
BaseURL: "https://www.shunyunbaoerp.com",
Username: "tester",
Password: "super-secret-password",
}
s := cfg.String()
if strings.Contains(s, "super-secret-password") {
t.Fatalf("SybConfig.String() 不能包含明文密码,实际: %s", s)
}
if strings.Contains(s, "tester") == false {
t.Errorf("String() 应该保留用户名等非敏感信息方便排查: %s", s)
}
}
func TestLoad_配置文件损坏时报明确错误(t *testing.T) {
_, err := loadFrom(t, "syb: [this is not a valid mapping")
if err == nil {
t.Fatal("格式错误的 YAML 应该返回错误")
}
}
2026-08-11 10:00:07 +08:00
func TestCatalogIntegrationConfig_环境覆盖且不泄露Token(t *testing.T) {
t.Setenv(catalogSourceEnv, "script-a")
t.Setenv(catalogTokenEnv, "0123456789abcdef0123456789abcdef")
cfg, err := mergeCatalogIntegration(CatalogIntegrationConfig{Source: "yaml", Token: "yaml-token-that-is-long-enough-123456"})
if err != nil {
t.Fatal(err)
}
if cfg.Source != "script-a" || cfg.Token != "0123456789abcdef0123456789abcdef" {
t.Fatalf("环境变量未覆盖:%s", cfg)
}
if strings.Contains(cfg.String(), cfg.Token) || !strings.Contains(cfg.String(), "****") {
t.Fatalf("配置字符串泄露 Token:%s", cfg)
}
}
func TestCatalogIntegrationConfig_弱Token拒绝且留空可禁用(t *testing.T) {
t.Setenv(catalogSourceEnv, "")
t.Setenv(catalogTokenEnv, "")
if cfg, err := mergeCatalogIntegration(CatalogIntegrationConfig{}); err != nil || cfg.Enabled() {
t.Fatalf("留空应安全禁用:cfg=%s err=%v", cfg, err)
}
if _, err := mergeCatalogIntegration(CatalogIntegrationConfig{Token: "too-short"}); err == nil || !strings.Contains(err.Error(), catalogTokenEnv) {
t.Fatalf("弱 Token 应被拒绝:%v", err)
}
}
func TestLoad_AI部署配置不包含APIKey(t *testing.T) {
cfg, err := loadFrom(t, `
ai:
secrets_path: "C:/secure/ai-secrets.yaml"
allowed_hosts:
- model.internal.example
`)
if err != nil {
t.Fatal(err)
}
if cfg.AI.SecretsPath != "C:/secure/ai-secrets.yaml" || len(cfg.AI.AllowedHosts) != 1 {
t.Fatalf("AI 部署配置解析错误: %+v", cfg.AI)
}
}