package config_test import ( "errors" "os" "path/filepath" "strings" "testing" "git.yuntuo.hk/k2safe/go-omni/config" mysql "git.yuntuo.hk/k2safe/go-omni/database/mysql" "git.yuntuo.hk/k2safe/go-omni/httpc" ) type moduleConfig struct { Database mysql.Config `json:"database"` Server config.Config `json:"server"` Client httpc.Config `json:"client"` } func (c moduleConfig) Validate() error { if err := c.Database.Validate(); err != nil { return config.AtField("database", err) } if err := c.Server.Validate(); err != nil { return config.AtField("server", err) } return config.AtField("client", c.Client.Validate()) } func TestModuleFieldDiagnostics(t *testing.T) { for _, tc := range []struct{ name, source, field, reason string }{ {"port", "database:\n port: 70000\n", "database.port", "1 and 65535"}, {"host", "database:\n host: 'invalid host'\n", "database.host", "hostname"}, {"pool", "database:\n pool:\n maxIdleConns: 999\n", "database.pool.maxIdleConns", "maxOpenConns"}, {"ping", "database:\n pool:\n pingTimeout: 0s\n", "database.pool.pingTimeout", "positive"}, {"timeout", "database:\n readTimeout: -1s\n", "database.readTimeout", "nonnegative"}, {"mysqlZone", "database:\n location: Secret/Not_A_Zone\n", "database.location", "IANA"}, {"tlsPair", "database:\n tls:\n certFile: secret-file\n keyFile: ''\n", "database.tls.keyFile", "certFile"}, {"level", "server:\n log:\n level: secret-level\n", "server.log.level", "debug"}, {"format", "server:\n log:\n format: secret-format\n", "server.log.format", "json or text"}, {"rotation", "server:\n log:\n rotation:\n maxBackups: -1\n", "server.log.rotation.maxBackups", "10000"}, {"zone", "server:\n log:\n timeZone: Secret/Not_A_Zone\n", "server.log.timeZone", "IANA"}, {"lifecycle", "server:\n lifecycle:\n stopTimeout: -1s\n", "server.lifecycle.stopTimeout", "nonnegative"}, {"policy", "server:\n request:\n body: secret-mode\n", "server.request.body", "required"}, {"clientPool", "client:\n maxIdleConns: -1\n", "client.maxIdleConns", "131072"}, {"proxy", "client:\n proxy:\n mode: url\n url: 'https://private:secret@localhost/path'\n", "client.proxy.url", "origin"}, } { t.Run(tc.name, func(t *testing.T) { path := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(path, []byte(tc.source), 0600); err != nil { t.Fatal(err) } _, err := config.LoadProject(path, moduleConfig{Database: mysql.DefaultConfig(), Server: config.Default()}) var d *config.Error if !errors.As(err, &d) || d.Field != tc.field || d.File != path || d.Line <= 1 || d.Column <= 0 || !strings.Contains(d.Reason, tc.reason) { t.Fatalf("diagnostic: %v", err) } if strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "Secret/") || strings.Contains(err.Error(), "private") { t.Fatalf("value leaked: %v", err) } }) } } func TestModuleDiagnosticsKeepReferenceAndCause(t *testing.T) { t.Setenv("CHECK_PORT", "70000") path := filepath.Join(t.TempDir(), "config.yaml") os.WriteFile(path, []byte("database:\n port: '${CHECK_PORT}'\n"), 0600) _, err := config.LoadProject(path, moduleConfig{Database: mysql.DefaultConfig(), Server: config.Default()}) var d *config.Error if !errors.As(err, &d) || d.Field != "database.port" || d.Env != "CHECK_PORT" || d.Line != 2 { t.Fatal(err) } original := mysql.DefaultConfig() original.Port = 70000 cause := original.Validate() wrapped := config.AtField("database", cause) if !errors.Is(wrapped, cause) { t.Fatal("lost original validation error") } // Unknown project errors continue to be redacted rather than echoed. wrapped = config.AtField("custom", errors.New("private-password")) if strings.Contains(wrapped.Error(), "private-password") { t.Fatal(wrapped) } }