Files
goauto/server/config/local_test.go
T
QiuSWandClaude Opus 5 72ecf25291 fix(#48): find config.yaml from server/ and stop diagnostics looking like errors
The startup log proved config.yaml was not being found during migrate.
Lookup covered the working directory and the executable's directory, but
the server runs from server/ while config.yaml sits at the repository
root — so it was found only when a launcher happened to export
GOAUTO_CONFIG. Anyone running `go run .` by hand got no local config at
all. Search the parent directory too, with the working directory still
winning.

The diagnostics also wrote to stderr, and the launcher pipes the server
through `2>&1 | Tee-Object`, which turns every stderr write into a
PowerShell NativeCommandError. The informational line I added to make
this debuggable was itself rendering as a red error block. They go to
stdout now.

Launchers set the console to UTF-8: Go writes UTF-8 while the console
decodes as the ANSI code page, which turned every Chinese log line into
mojibake.

Verified by running the built binary from server/ with no GOAUTO_CONFIG
set: it loads ../config.yaml and the lines survive 2>/dev/null.

Not verified: the two PowerShell edits, which need a Windows run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:44:03 +08:00

273 lines
8.5 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
sdkconfig "github.com/go-admin-team/go-admin-core/sdk/config"
)
func writeLocalConfig(t *testing.T, body string) string {
t.Helper()
path := filepath.Join(t.TempDir(), LocalConfigName)
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("写入配置失败: %v", err)
}
t.Setenv("GOAUTO_CONFIG", path)
return path
}
func restoreConfigState(t *testing.T) {
t.Helper()
database := *sdkconfig.DatabaseConfig
application := *sdkconfig.ApplicationConfig
ext := ExtConfig.SYB
t.Cleanup(func() {
*sdkconfig.DatabaseConfig = database
*sdkconfig.ApplicationConfig = application
ExtConfig.SYB = ext
})
}
func TestApplyLocalConfigReadsDatabasePortsAndSYB(t *testing.T) {
restoreConfigState(t)
writeLocalConfig(t, `
database:
host: 127.0.0.1
port: 3307
user: root
password: "123456"
name: goauto
ports:
server: 8010
web: 9527
syb:
username: operator
password: "654321"
`)
ApplyLocalConfig()
if sdkconfig.DatabaseConfig.Driver != "mysql" {
t.Fatalf("driver 不对: %q", sdkconfig.DatabaseConfig.Driver)
}
want := "root:123456@tcp(127.0.0.1:3307)/goauto?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s"
if sdkconfig.DatabaseConfig.Source != want {
t.Fatalf("DSN 不对:\n 实际 %s\n 期望 %s", sdkconfig.DatabaseConfig.Source, want)
}
if sdkconfig.ApplicationConfig.Port != 8010 {
t.Fatalf("端口不对: %d", sdkconfig.ApplicationConfig.Port)
}
if ExtConfig.SYB.Username != "operator" || ExtConfig.SYB.Password != "654321" {
t.Fatalf("SYB 凭据没有读进来: %+v", ExtConfig.SYB)
}
}
// `[必须]` 纯数字密码不加引号会被 YAML 读成整数。这是上游踩过的坑,
// 也是用户第一版 config.yaml 的实际写法——绝不能因此启动失败或丢值。
func TestApplyLocalConfigToleratesUnquotedNumericPassword(t *testing.T) {
restoreConfigState(t)
writeLocalConfig(t, `
database:
host: 127.0.0.1
port: 3307
user: root
password: 123456
name: goauto
syb:
username: operator
password: 654321
`)
ApplyLocalConfig()
if got := sdkconfig.DatabaseConfig.Source; got != "root:123456@tcp(127.0.0.1:3307)/goauto?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s" {
t.Fatalf("数字密码没有被正确还原: %s", got)
}
if ExtConfig.SYB.Password != "654321" {
t.Fatalf("SYB 数字密码没有被正确还原: %q", ExtConfig.SYB.Password)
}
}
// 分层的核心:环境变量必须压过 config.yaml。
func TestEnvironmentOverridesLocalConfig(t *testing.T) {
restoreConfigState(t)
writeLocalConfig(t, `
database:
host: 127.0.0.1
port: 3307
user: root
password: "fromfile"
name: goauto
ports:
server: 8010
syb:
username: from-file
password: "from-file"
`)
t.Setenv("GOAUTO_DB_DSN", "envuser:envpass@tcp(10.0.0.1:3306)/envdb")
t.Setenv("GOAUTO_SERVER_PORT", "9000")
t.Setenv("GOAUTO_SYB_USERNAME", "from-env")
t.Setenv("GOAUTO_SYB_PASSWORD", "from-env-pass")
// 真实启动顺序:先 config.yaml,再环境变量。
ApplyLocalConfig()
ApplyEnvironment()
if sdkconfig.DatabaseConfig.Source != "envuser:envpass@tcp(10.0.0.1:3306)/envdb" {
t.Fatalf("环境变量没有压过 config.yaml: %s", sdkconfig.DatabaseConfig.Source)
}
if sdkconfig.ApplicationConfig.Port != 9000 {
t.Fatalf("端口应以环境变量为准: %d", sdkconfig.ApplicationConfig.Port)
}
if ExtConfig.SYB.Username != "from-env" || ExtConfig.SYB.Password != "from-env-pass" {
t.Fatalf("SYB 凭据应以环境变量为准: %+v", ExtConfig.SYB)
}
}
// 只写凭据、不写其它项时,settings.yml 里的运维参数必须原样保留。
func TestLocalConfigWithOnlyCredentialsKeepsSettingsValues(t *testing.T) {
restoreConfigState(t)
ExtConfig.SYB = SYB{BaseURL: "https://from-settings", PageSize: 20, MaxMatches: 10000, OcrURL: "https://ocr", OcrMaxAttempts: 5}
writeLocalConfig(t, `
syb:
username: operator
password: "654321"
`)
ApplyLocalConfig()
if ExtConfig.SYB.BaseURL != "https://from-settings" || ExtConfig.SYB.PageSize != 20 ||
ExtConfig.SYB.MaxMatches != 10000 || ExtConfig.SYB.OcrURL != "https://ocr" || ExtConfig.SYB.OcrMaxAttempts != 5 {
t.Fatalf("settings.yml 的非机密项被意外覆盖了: %+v", ExtConfig.SYB)
}
if ExtConfig.SYB.Username != "operator" {
t.Fatalf("凭据没有生效: %+v", ExtConfig.SYB)
}
}
// 容器场景根本没有 config.yaml,必须安静地当作正常情况。
func TestAbsentLocalConfigIsASilentNoOp(t *testing.T) {
restoreConfigState(t)
t.Setenv("GOAUTO_CONFIG", "")
if path := LocalConfigPath(); path != "" {
t.Fatalf("测试环境不应找到 config.yaml,实际找到 %s", path)
}
ApplyLocalConfig()
}
// 但显式指定了 GOAUTO_CONFIG 却指不到文件,是配置错误,要给出提示——
// 这和"压根没有这个文件"是两回事,不能一起静默掉。
func TestExplicitlyNamedMissingConfigStillStartsButIsReported(t *testing.T) {
restoreConfigState(t)
missing := filepath.Join(t.TempDir(), "does-not-exist.yaml")
t.Setenv("GOAUTO_CONFIG", missing)
if LocalConfigPath() != missing {
t.Fatal("显式指定的路径应原样返回,好让读取失败时报出来")
}
ApplyLocalConfig()
}
// 配置文件写坏了也不能让服务起不来——它提供的每一项都是可选的。
func TestMalformedLocalConfigIsIgnored(t *testing.T) {
restoreConfigState(t)
writeLocalConfig(t, "database: [this is not a mapping\n")
ApplyLocalConfig()
}
// 数据库段缺字段时不能拼出半截 DSN,宁可完全不动。
func TestIncompleteDatabaseSectionIsIgnored(t *testing.T) {
restoreConfigState(t)
sdkconfig.DatabaseConfig.Source = "untouched"
writeLocalConfig(t, `
database:
host: 127.0.0.1
user: root
`)
ApplyLocalConfig()
if sdkconfig.DatabaseConfig.Source != "untouched" {
t.Fatalf("字段不全时不应改写 DSN: %s", sdkconfig.DatabaseConfig.Source)
}
}
func TestScalarRendersYAMLTypesWithoutExponentOrTrailingZero(t *testing.T) {
values := map[string]any{"int": 3307, "float": float64(8010), "str": "x", "bool": true, "big": float64(1e7)}
for key, want := range map[string]string{
"int": "3307", "float": "8010", "str": "x", "bool": "true", "big": "10000000", "missing": "",
} {
if got := scalar(values, key); got != want {
t.Fatalf("%s: 期望 %q,实际 %q", key, want, got)
}
}
}
// 报错信息必须指向操作员真正编辑的那个文件。只提环境变量会把人引到错的地方。
func TestLocalConfigPathIsReportableWhenAbsent(t *testing.T) {
restoreConfigState(t)
t.Setenv("GOAUTO_CONFIG", "")
if LocalConfigPath() != "" {
t.Fatal("测试环境不应找到 config.yaml")
}
}
func TestLocalConfigPathPrefersExplicitEnvOverCwd(t *testing.T) {
restoreConfigState(t)
explicit := writeLocalConfig(t, "syb:\n username: a\n password: \"b\"\n")
if got := LocalConfigPath(); got != explicit {
t.Fatalf("GOAUTO_CONFIG 应优先于其它位置: %q", got)
}
}
// 服务端通常从 server/ 启动,而 config.yaml 在仓库根。没有这条上级目录查找,
// 手工跑 `go run .` 就完全读不到本地配置——启动脚本导出 GOAUTO_CONFIG 只是
// 恰好掩盖了这一点。
func TestLocalConfigPathFindsFileInParentDirectory(t *testing.T) {
restoreConfigState(t)
t.Setenv("GOAUTO_CONFIG", "")
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, LocalConfigName),
[]byte("syb:\n username: operator\n password: \"654321\"\n"), 0o600); err != nil {
t.Fatalf("写配置失败: %v", err)
}
child := filepath.Join(root, "server")
if err := os.Mkdir(child, 0o755); err != nil {
t.Fatalf("建目录失败: %v", err)
}
previous, err := os.Getwd()
if err != nil {
t.Fatalf("取工作目录失败: %v", err)
}
if err := os.Chdir(child); err != nil {
t.Fatalf("切目录失败: %v", err)
}
t.Cleanup(func() { os.Chdir(previous) })
ApplyLocalConfig()
if !ExtConfig.SYB.HasCredentials() {
t.Fatal("从 server/ 启动时应能在上级目录找到 config.yaml")
}
}
// 当前目录优先于上级目录。
func TestLocalConfigPathPrefersCwdOverParent(t *testing.T) {
restoreConfigState(t)
t.Setenv("GOAUTO_CONFIG", "")
root := t.TempDir()
os.WriteFile(filepath.Join(root, LocalConfigName), []byte("syb:\n username: parent\n password: \"1\"\n"), 0o600)
child := filepath.Join(root, "server")
os.Mkdir(child, 0o755)
os.WriteFile(filepath.Join(child, LocalConfigName), []byte("syb:\n username: cwd\n password: \"1\"\n"), 0o600)
previous, _ := os.Getwd()
os.Chdir(child)
t.Cleanup(func() { os.Chdir(previous) })
ApplyLocalConfig()
if ExtConfig.SYB.Username != "cwd" {
t.Fatalf("当前目录应优先: %q", ExtConfig.SYB.Username)
}
}