Files
cmautobuy/admin/config/config_test.go
T

304 lines
8.9 KiB
Go
Raw 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 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
}
// 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 应该返回错误")
}
}