feat: add precise diagnostics and optional client copy logging controls
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/internal/configdiag"
|
||||
)
|
||||
|
||||
// Error is a value-free diagnostic. Cause may contain private parser details;
|
||||
@@ -56,6 +58,10 @@ func validationError(err error) *Error {
|
||||
out.cause = err
|
||||
return &out
|
||||
}
|
||||
var nested *configdiag.Error
|
||||
if errors.As(err, &nested) {
|
||||
return &Error{Field: nested.Field, Reason: nested.Reason, cause: err}
|
||||
}
|
||||
var c *ConfigError
|
||||
if errors.As(err, &c) {
|
||||
return &Error{Field: c.Field, Reason: c.Message, cause: err}
|
||||
|
||||
85
config/moduleDiagnostics_test.go
Normal file
85
config/moduleDiagnostics_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user