Files
goauto/server/config/extend_test.go
T
QiuSWandClaude Opus 5 a31c4776c4 feat(#48): port SYB ERP client, session store and config (Stage 1)
Port the SYB ERP HTTP client from the upstream cmautobuy project. The
package is stdlib-only (no DB, no gin), so client.go, columns.go, ocr.go
and their httptest suites carry over almost verbatim; only the package
name and doc references changed.

Add on top of the port:
  - models.SYBSession + gorm SessionStore, so a restart does not force a
    fresh captcha. Cookies are credential-equivalent and carry json:"-".
    Expired sessions read as absent because SYB has no rolling renewal.
  - config.Extend.SYB. Non-secret settings live in settings.yml;
    username and password come only from GOAUTO_SYB_* environment
    variables, so no credential lands in a tracked file.
  - docs/12-syb-erp-interface.md, the ported interface contract.

The guard tests were mutation-checked: reverting json:"-" and renaming a
settings.yml key each make their test fail.

No import endpoint yet — that needs real credentials and live network
verification, which is Stage 2 of #48.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:30:35 +08:00

121 lines
4.1 KiB
Go

package config
import (
"testing"
sdkconfig "github.com/go-admin-team/go-admin-core/sdk/config"
)
func TestApplyEnvironment(t *testing.T) {
originalDefault := *sdkconfig.DatabaseConfig
originalDatabases := sdkconfig.DatabasesConfig
originalApplication := *sdkconfig.ApplicationConfig
t.Cleanup(func() {
*sdkconfig.DatabaseConfig = originalDefault
sdkconfig.DatabasesConfig = originalDatabases
*sdkconfig.ApplicationConfig = originalApplication
})
sdkconfig.DatabasesConfig = map[string]*sdkconfig.Database{"*": {Driver: "sqlite3", Source: "old.db"}}
t.Setenv("GOAUTO_DB_DSN", "user:secret@tcp(127.0.0.1:3306)/goauto")
t.Setenv("GOAUTO_DB_DRIVER", "mysql")
t.Setenv("GOAUTO_SERVER_PORT", "18000")
ApplyEnvironment()
if sdkconfig.DatabaseConfig.Driver != "mysql" || sdkconfig.DatabaseConfig.Source == "" {
t.Fatal("default database was not overridden")
}
if database := sdkconfig.DatabasesConfig["*"]; database.Driver != "mysql" || database.Source != sdkconfig.DatabaseConfig.Source {
t.Fatal("host database was not overridden")
}
if sdkconfig.ApplicationConfig.Port != 18000 {
t.Fatalf("application port = %d, want 18000", sdkconfig.ApplicationConfig.Port)
}
}
func TestApplyEnvironmentIgnoresEmptyDatabaseAndInvalidPort(t *testing.T) {
original := *sdkconfig.DatabaseConfig
originalApplication := *sdkconfig.ApplicationConfig
t.Cleanup(func() {
*sdkconfig.DatabaseConfig = original
*sdkconfig.ApplicationConfig = originalApplication
})
sdkconfig.DatabaseConfig.Driver = "sqlite3"
sdkconfig.DatabaseConfig.Source = "local.db"
sdkconfig.ApplicationConfig.Port = 8000
t.Setenv("GOAUTO_DB_DSN", "")
t.Setenv("GOAUTO_SERVER_PORT", "70000")
ApplyEnvironment()
if sdkconfig.DatabaseConfig.Driver != "sqlite3" || sdkconfig.DatabaseConfig.Source != "local.db" {
t.Fatal("empty environment unexpectedly changed database config")
}
if sdkconfig.ApplicationConfig.Port != 8000 {
t.Fatal("invalid environment unexpectedly changed application port")
}
}
// 凭据只能来自环境变量:settings.yml 里不放账号密码,避免它们进 Git。
func TestApplyEnvironmentLoadsSYBCredentials(t *testing.T) {
original := ExtConfig.SYB
t.Cleanup(func() { ExtConfig.SYB = original })
t.Setenv("GOAUTO_SYB_USERNAME", " operator ")
t.Setenv("GOAUTO_SYB_PASSWORD", " se cret ")
ApplyEnvironment()
if ExtConfig.SYB.Username != "operator" {
t.Fatalf("账号应去掉首尾空白: %q", ExtConfig.SYB.Username)
}
// 密码原样保留:首尾空白可能是密码的一部分,剪掉就是悄悄改了凭据。
if ExtConfig.SYB.Password != " se cret " {
t.Fatalf("密码不应被修改: %q", ExtConfig.SYB.Password)
}
}
func TestSYBResolvedFillsBlanksButNeverInventsCredentials(t *testing.T) {
resolved := SYB{}.Resolved()
if resolved.BaseURL != DefaultSYBBaseURL {
t.Fatalf("BaseURL 默认值不对: %q", resolved.BaseURL)
}
if resolved.PageSize != DefaultSYBPageSize || resolved.MaxMatches != DefaultSYBMaxMatches {
t.Fatalf("分页默认值不对: %+v", resolved)
}
if resolved.OcrMaxAttempts != DefaultSYBOcrMaxAttempts {
t.Fatalf("OCR 重试次数默认值不对: %d", resolved.OcrMaxAttempts)
}
if resolved.Username != "" || resolved.Password != "" {
t.Fatal("Resolved 不得给账号密码编造默认值")
}
// ocr_url 留空是合法配置(降级到手工输入),不能被填上默认值。
if resolved.OcrURL != "" {
t.Fatalf("空 OcrURL 不应被填充: %q", resolved.OcrURL)
}
}
func TestSYBResolvedNormalisesBaseURLTrailingSlash(t *testing.T) {
if got := (SYB{BaseURL: " https://example.com/ "}).Resolved().BaseURL; got != "https://example.com" {
t.Fatalf("BaseURL 规范化不对: %q", got)
}
}
func TestSYBHasCredentialsRequiresBothFields(t *testing.T) {
for _, c := range []struct {
name string
syb SYB
want bool
}{
{"都有", SYB{Username: "a", Password: "b"}, true},
{"缺密码", SYB{Username: "a"}, false},
{"缺账号", SYB{Password: "b"}, false},
{"账号只有空白", SYB{Username: " ", Password: "b"}, false},
{"都没有", SYB{}, false},
} {
if got := c.syb.HasCredentials(); got != c.want {
t.Fatalf("%s: 期望 %v,实际 %v", c.name, c.want, got)
}
}
}