fix(config): layer root .env over configs/.env.{ENV} for KB auth
Stop using a fake default KB password; merge Compose-style env files and rewrite compose DNS to localhost on the host so go test/E2E authenticate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+188
-128
@@ -93,7 +93,7 @@ func Load(path string) (*Config, error) {
|
||||
KbDbHost: "127.0.0.1",
|
||||
KbDbPort: 5432,
|
||||
KbDbUser: "regkb",
|
||||
KbDbPassword: "shared_kb_secret_pass",
|
||||
KbDbPassword: "", // 禁止静默默认口令;须由根 .env / 进程环境提供
|
||||
KbDbName: "regulations",
|
||||
|
||||
ContextDbHost: "127.0.0.1",
|
||||
@@ -120,134 +120,18 @@ func Load(path string) (*Config, error) {
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
if _, err := os.Stat("configs/.env.dev"); err == nil {
|
||||
path = "configs/.env.dev"
|
||||
} else if _, err := os.Stat(".env"); err == nil {
|
||||
path = ".env"
|
||||
// 与 Compose 一致:根 .env(公共密钥)+ configs/.env.{ENV}(环境差异 overlay)
|
||||
_ = applyEnvFile(cfg, ".env")
|
||||
envName := cfg.Env
|
||||
if v := os.Getenv("ENV"); v != "" {
|
||||
envName = v
|
||||
}
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
if commentIdx := strings.Index(val, " #"); commentIdx != -1 {
|
||||
val = strings.TrimSpace(val[:commentIdx])
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "ENV":
|
||||
cfg.Env = val
|
||||
case "DATA_DIR":
|
||||
cfg.DataDir = val
|
||||
case "APP_PUBLISH_HOST", "HTTP_HOST":
|
||||
cfg.HTTPHost = val
|
||||
case "HTTP_PORT":
|
||||
if p, err := strconv.Atoi(val); err == nil {
|
||||
cfg.HTTPPort = p
|
||||
}
|
||||
case "AGENT_BACKEND":
|
||||
cfg.AgentBackend = val
|
||||
case "AGENT_TIMEOUT":
|
||||
if d, err := time.ParseDuration(val); err == nil {
|
||||
cfg.AgentTimeout = d
|
||||
}
|
||||
case "CURSOR_API_KEY":
|
||||
cfg.CursorAPIKey = val
|
||||
case "CURSOR_MODEL":
|
||||
cfg.CursorModel = val
|
||||
case "CURSOR_BIN":
|
||||
cfg.CursorBin = val
|
||||
case "CURSOR_BRIDGE_BIN":
|
||||
cfg.CursorBridgeBin = val
|
||||
case "PI_AGENT_BIN":
|
||||
cfg.PiAgentBin = val
|
||||
case "PI_AGENT_BRIDGE_BIN":
|
||||
cfg.PiAgentBridgeBin = val
|
||||
case "PI_AGENT_PROVIDER":
|
||||
cfg.PiAgentProvider = val
|
||||
case "PI_AGENT_MODEL":
|
||||
cfg.PiAgentModel = val
|
||||
case "PI_AGENT_RUNNER_URL":
|
||||
cfg.PiAgentRunnerURL = val
|
||||
case "KB_DB_HOST":
|
||||
cfg.KbDbHost = val
|
||||
case "KB_DB_PORT":
|
||||
if p, err := strconv.Atoi(val); err == nil {
|
||||
cfg.KbDbPort = p
|
||||
}
|
||||
case "KB_DB_USER":
|
||||
cfg.KbDbUser = val
|
||||
case "KB_DB_PASSWORD":
|
||||
cfg.KbDbPassword = val
|
||||
case "KB_DB_NAME":
|
||||
cfg.KbDbName = val
|
||||
case "CONTEXT_DB_HOST":
|
||||
cfg.ContextDbHost = val
|
||||
case "CONTEXT_DB_PORT":
|
||||
if p, err := strconv.Atoi(val); err == nil {
|
||||
cfg.ContextDbPort = p
|
||||
}
|
||||
case "CONTEXT_DB_USER":
|
||||
cfg.ContextDbUser = val
|
||||
case "CONTEXT_DB_PASSWORD":
|
||||
cfg.ContextDbPassword = val
|
||||
case "CONTEXT_DB_NAME":
|
||||
cfg.ContextDbName = val
|
||||
case "EMBEDDING_ENDPOINT_URL":
|
||||
cfg.EmbeddingEndpointURL = val
|
||||
case "EMBEDDING_PROVIDER":
|
||||
cfg.EmbeddingProvider = val
|
||||
case "EMBEDDING_MODEL":
|
||||
cfg.EmbeddingModel = val
|
||||
case "EMBEDDING_API_KEY":
|
||||
cfg.EmbeddingAPIKey = val
|
||||
case "RERANKER_ENDPOINT_URL":
|
||||
cfg.RerankerEndpointURL = val
|
||||
case "RERANKER_PROVIDER":
|
||||
cfg.RerankerProvider = val
|
||||
case "RERANKER_MODEL":
|
||||
cfg.RerankerModel = val
|
||||
case "RERANKER_API_KEY":
|
||||
cfg.RerankerAPIKey = val
|
||||
case "ADMIN_USERNAME":
|
||||
cfg.AdminUsername = val
|
||||
case "ADMIN_PASSWORD":
|
||||
cfg.AdminPassword = val
|
||||
case "AUTH_REGISTRATION_ENABLED":
|
||||
cfg.AuthRegistrationEnabled = val == "true" || val == "1"
|
||||
case "AUTH_REQUIRE_APPROVAL":
|
||||
cfg.AuthRequireApproval = val == "true" || val == "1"
|
||||
case "MAX_CONCURRENT_AGENTS":
|
||||
if n, err := strconv.Atoi(val); err == nil && n > 0 {
|
||||
cfg.MaxConcurrentAgents = n
|
||||
}
|
||||
case "ALLOW_IN_MEMORY":
|
||||
cfg.AllowInMemory = val == "true" || val == "1"
|
||||
case "CORS_ALLOWED_ORIGINS":
|
||||
cfg.CORSAllowedOrigins = parseCSVOrigins(val)
|
||||
case "VAULT_STATE_ROOT":
|
||||
cfg.VaultStateRoot = val
|
||||
case "VAULT_STORAGE_IDENTITY":
|
||||
cfg.VaultStorageIdentity = val
|
||||
case "VAULT_MASTER_KEY":
|
||||
cfg.VaultMasterKey = val
|
||||
case "FAV_DEGRADED_READ":
|
||||
cfg.FAVDegradedRead = val == "true" || val == "1"
|
||||
case "FAV_PUBLISHER_ENABLED":
|
||||
cfg.FAVPublisherEnabled = val == "true" || val == "1"
|
||||
}
|
||||
if envName == "" {
|
||||
envName = "dev"
|
||||
}
|
||||
_ = applyEnvFile(cfg, filepath.Join("configs", ".env."+envName))
|
||||
} else {
|
||||
_ = applyEnvFile(cfg, path)
|
||||
}
|
||||
|
||||
// 允许系统环境变量覆盖
|
||||
@@ -406,6 +290,7 @@ func Load(path string) (*Config, error) {
|
||||
}
|
||||
applyDSNOverrides(cfg)
|
||||
applyCORSDefaults(cfg)
|
||||
NormalizeKBHostForRuntime(cfg)
|
||||
|
||||
if err := validateProductionSecrets(cfg); err != nil {
|
||||
return nil, err
|
||||
@@ -417,6 +302,162 @@ func Load(path string) (*Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// applyEnvFile overlays KEY=VALUE pairs from path onto cfg. Missing file is a no-op.
|
||||
func applyEnvFile(cfg *Config, path string) error {
|
||||
if cfg == nil || strings.TrimSpace(path) == "" {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
val := strings.TrimSpace(parts[1])
|
||||
if commentIdx := strings.Index(val, " #"); commentIdx != -1 {
|
||||
val = strings.TrimSpace(val[:commentIdx])
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "ENV":
|
||||
cfg.Env = val
|
||||
case "DATA_DIR":
|
||||
cfg.DataDir = val
|
||||
case "APP_PUBLISH_HOST", "HTTP_HOST":
|
||||
cfg.HTTPHost = val
|
||||
case "HTTP_PORT":
|
||||
if p, err := strconv.Atoi(val); err == nil {
|
||||
cfg.HTTPPort = p
|
||||
}
|
||||
case "AGENT_BACKEND":
|
||||
cfg.AgentBackend = val
|
||||
case "AGENT_TIMEOUT":
|
||||
if d, err := time.ParseDuration(val); err == nil {
|
||||
cfg.AgentTimeout = d
|
||||
}
|
||||
case "CURSOR_API_KEY":
|
||||
cfg.CursorAPIKey = val
|
||||
case "CURSOR_MODEL":
|
||||
cfg.CursorModel = val
|
||||
case "CURSOR_BIN":
|
||||
cfg.CursorBin = val
|
||||
case "CURSOR_BRIDGE_BIN":
|
||||
cfg.CursorBridgeBin = val
|
||||
case "PI_AGENT_BIN":
|
||||
cfg.PiAgentBin = val
|
||||
case "PI_AGENT_BRIDGE_BIN":
|
||||
cfg.PiAgentBridgeBin = val
|
||||
case "PI_AGENT_PROVIDER":
|
||||
cfg.PiAgentProvider = val
|
||||
case "PI_AGENT_MODEL":
|
||||
cfg.PiAgentModel = val
|
||||
case "PI_AGENT_RUNNER_URL":
|
||||
cfg.PiAgentRunnerURL = val
|
||||
case "KB_DB_HOST":
|
||||
cfg.KbDbHost = val
|
||||
case "KB_DB_PORT":
|
||||
if p, err := strconv.Atoi(val); err == nil {
|
||||
cfg.KbDbPort = p
|
||||
}
|
||||
case "KB_DB_USER":
|
||||
cfg.KbDbUser = val
|
||||
case "KB_DB_PASSWORD":
|
||||
cfg.KbDbPassword = val
|
||||
case "KB_DB_NAME":
|
||||
cfg.KbDbName = val
|
||||
case "CONTEXT_DB_HOST":
|
||||
cfg.ContextDbHost = val
|
||||
case "CONTEXT_DB_PORT":
|
||||
if p, err := strconv.Atoi(val); err == nil {
|
||||
cfg.ContextDbPort = p
|
||||
}
|
||||
case "CONTEXT_DB_USER":
|
||||
cfg.ContextDbUser = val
|
||||
case "CONTEXT_DB_PASSWORD":
|
||||
cfg.ContextDbPassword = val
|
||||
case "CONTEXT_DB_NAME":
|
||||
cfg.ContextDbName = val
|
||||
case "EMBEDDING_ENDPOINT_URL":
|
||||
cfg.EmbeddingEndpointURL = val
|
||||
case "EMBEDDING_PROVIDER":
|
||||
cfg.EmbeddingProvider = val
|
||||
case "EMBEDDING_MODEL":
|
||||
cfg.EmbeddingModel = val
|
||||
case "EMBEDDING_API_KEY":
|
||||
cfg.EmbeddingAPIKey = val
|
||||
case "RERANKER_ENDPOINT_URL":
|
||||
cfg.RerankerEndpointURL = val
|
||||
case "RERANKER_PROVIDER":
|
||||
cfg.RerankerProvider = val
|
||||
case "RERANKER_MODEL":
|
||||
cfg.RerankerModel = val
|
||||
case "RERANKER_API_KEY":
|
||||
cfg.RerankerAPIKey = val
|
||||
case "ADMIN_USERNAME":
|
||||
cfg.AdminUsername = val
|
||||
case "ADMIN_PASSWORD":
|
||||
cfg.AdminPassword = val
|
||||
case "AUTH_REGISTRATION_ENABLED":
|
||||
cfg.AuthRegistrationEnabled = val == "true" || val == "1"
|
||||
case "AUTH_REQUIRE_APPROVAL":
|
||||
cfg.AuthRequireApproval = val == "true" || val == "1"
|
||||
case "MAX_CONCURRENT_AGENTS":
|
||||
if n, err := strconv.Atoi(val); err == nil && n > 0 {
|
||||
cfg.MaxConcurrentAgents = n
|
||||
}
|
||||
case "ALLOW_IN_MEMORY":
|
||||
cfg.AllowInMemory = val == "true" || val == "1"
|
||||
case "CORS_ALLOWED_ORIGINS":
|
||||
cfg.CORSAllowedOrigins = parseCSVOrigins(val)
|
||||
case "VAULT_STATE_ROOT":
|
||||
cfg.VaultStateRoot = val
|
||||
case "VAULT_STORAGE_IDENTITY":
|
||||
cfg.VaultStorageIdentity = val
|
||||
case "VAULT_MASTER_KEY":
|
||||
cfg.VaultMasterKey = val
|
||||
case "FAV_DEGRADED_READ":
|
||||
cfg.FAVDegradedRead = val == "true" || val == "1"
|
||||
case "FAV_PUBLISHER_ENABLED":
|
||||
cfg.FAVPublisherEnabled = val == "true" || val == "1"
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// NormalizeKBHostForRuntime rewrites Compose DNS names to localhost when running
|
||||
// on the host (no /.dockerenv). Inside containers the shared network name is kept.
|
||||
func NormalizeKBHostForRuntime(cfg *Config) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||
return
|
||||
}
|
||||
switch strings.TrimSpace(cfg.KbDbHost) {
|
||||
case "shuimu-kb-shared", "shuimu-kb":
|
||||
cfg.KbDbHost = "127.0.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
// RequireKbPassword returns an error when KB_DB_PASSWORD is empty (common after
|
||||
// layered Load if root .env is missing). Callers that need online KB should check this.
|
||||
func (c *Config) RequireKbPassword() error {
|
||||
if c == nil || strings.TrimSpace(c.KbDbPassword) == "" {
|
||||
return fmt.Errorf("config: KB_DB_PASSWORD is empty; provide it in root .env (layered with configs/.env.{ENV}) or process environment")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateIndependentVault refuses vault roots that collide with runtime data/{env}.
|
||||
// Empty root is allowed (FAV stays unconfigured / disabled).
|
||||
func ValidateIndependentVault(root, dataDir string) error {
|
||||
@@ -550,11 +591,30 @@ func parsePostgresDSN(dsn string) (host string, port int, user, password, dbName
|
||||
return host, port, user, password, dbName, true
|
||||
}
|
||||
|
||||
// KbDSN 返回 PostgreSQL 连接字符串
|
||||
// KbDSN 返回 PostgreSQL 连接字符串。口令为空时仍返回可解析 DSN,但不应被静默当真口令使用。
|
||||
func (c *Config) KbDSN() string {
|
||||
return "postgres://" + c.KbDbUser + ":" + c.KbDbPassword + "@" + c.KbDbHost + ":" + strconv.Itoa(c.KbDbPort) + "/" + c.KbDbName + "?sslmode=disable"
|
||||
}
|
||||
|
||||
// RedactPostgresDSN masks the password segment of a postgres:// DSN for logs.
|
||||
func RedactPostgresDSN(dsn string) string {
|
||||
if !strings.HasPrefix(dsn, "postgres://") {
|
||||
return dsn
|
||||
}
|
||||
rest := strings.TrimPrefix(dsn, "postgres://")
|
||||
at := strings.LastIndex(rest, "@")
|
||||
if at <= 0 {
|
||||
return "postgres://***"
|
||||
}
|
||||
creds := rest[:at]
|
||||
hostPart := rest[at+1:]
|
||||
userPass := strings.SplitN(creds, ":", 2)
|
||||
if len(userPass) != 2 {
|
||||
return "postgres://***@" + hostPart
|
||||
}
|
||||
return "postgres://" + userPass[0] + ":***@" + hostPart
|
||||
}
|
||||
|
||||
// ContextDSN 返回专属记忆与多级上下文 PostgreSQL 连接字符串
|
||||
func (c *Config) ContextDSN() string {
|
||||
return "postgres://" + c.ContextDbUser + ":" + c.ContextDbPassword + "@" + c.ContextDbHost + ":" + strconv.Itoa(c.ContextDbPort) + "/" + c.ContextDbName + "?sslmode=disable"
|
||||
|
||||
@@ -3,6 +3,7 @@ package config_test
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -10,6 +11,17 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigLoadDefaultsAndFile(t *testing.T) {
|
||||
t.Setenv("ENV", "")
|
||||
t.Setenv("DATA_DIR", "")
|
||||
t.Setenv("AGENT_BACKEND", "")
|
||||
t.Setenv("AGENT_TIMEOUT", "")
|
||||
t.Setenv("KB_DB_PORT", "")
|
||||
t.Setenv("KB_DB_PASSWORD", "")
|
||||
t.Setenv("KB_DB_HOST", "")
|
||||
t.Setenv("POSTGRES_SHARED_KB_DSN", "")
|
||||
t.Setenv("APP_DB_DSN", "")
|
||||
t.Setenv("HTTP_ADDR", "")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
envPath := filepath.Join(tmpDir, ".env")
|
||||
envContent := `ENV=dev
|
||||
@@ -58,8 +70,13 @@ func TestConfigLoadDSNOverrides(t *testing.T) {
|
||||
if cfg.ContextDbHost != "system-db" || cfg.ContextDbPort != 5432 || cfg.ContextDbUser != "app_user" || cfg.ContextDbName != "agent_system_dev" {
|
||||
t.Fatalf("unexpected context db config: %+v", cfg)
|
||||
}
|
||||
if cfg.KbDbHost != "shuimu-kb-shared" || cfg.KbDbPort != 5432 || cfg.KbDbUser != "kb_user" || cfg.KbDbName != "regulations" {
|
||||
t.Fatalf("unexpected kb db config: %+v", cfg)
|
||||
// 宿主机运行时会将 Compose DNS 归一化为 127.0.0.1(容器内保留原名)
|
||||
wantHost := "shuimu-kb-shared"
|
||||
if _, err := os.Stat("/.dockerenv"); err != nil {
|
||||
wantHost = "127.0.0.1"
|
||||
}
|
||||
if cfg.KbDbHost != wantHost || cfg.KbDbPort != 5432 || cfg.KbDbUser != "kb_user" || cfg.KbDbName != "regulations" {
|
||||
t.Fatalf("unexpected kb db config: host=%s want=%s cfg=%+v", cfg.KbDbHost, wantHost, cfg)
|
||||
}
|
||||
if cfg.HTTPPort != 9090 || cfg.HTTPHost != "0.0.0.0" {
|
||||
t.Fatalf("expected HTTP 0.0.0.0:9090, got %s:%d", cfg.HTTPHost, cfg.HTTPPort)
|
||||
@@ -184,3 +201,108 @@ func TestValidateIndependentVault(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigDefaultKbPasswordEmpty(t *testing.T) {
|
||||
t.Setenv("KB_DB_PASSWORD", "")
|
||||
t.Setenv("KB_DB_HOST", "")
|
||||
t.Setenv("POSTGRES_SHARED_KB_DSN", "")
|
||||
cfg, err := config.Load("/dev/null")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.KbDbPassword != "" {
|
||||
t.Fatalf("expected empty default KB password, got %q", cfg.KbDbPassword)
|
||||
}
|
||||
if err := cfg.RequireKbPassword(); err == nil {
|
||||
t.Fatal("expected RequireKbPassword to fail when empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigLayeredLoadMergesRootPassword(t *testing.T) {
|
||||
t.Setenv("KB_DB_PASSWORD", "")
|
||||
t.Setenv("KB_DB_HOST", "")
|
||||
t.Setenv("KB_DB_PORT", "")
|
||||
t.Setenv("KB_DB_USER", "")
|
||||
t.Setenv("POSTGRES_SHARED_KB_DSN", "")
|
||||
t.Setenv("ENV", "")
|
||||
t.Setenv("DATA_DIR", "")
|
||||
|
||||
root := t.TempDir()
|
||||
old, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chdir(old) })
|
||||
|
||||
if err := os.MkdirAll("configs", 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(".env", []byte("KB_DB_PASSWORD=layered-real-pass\nKB_DB_HOST=shuimu-kb-shared\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile("configs/.env.dev", []byte("ENV=dev\nKB_DB_PORT=5432\nKB_DB_USER=regkb\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := config.Load("")
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.KbDbPassword != "layered-real-pass" {
|
||||
t.Fatalf("expected layered password from root .env, got %q", cfg.KbDbPassword)
|
||||
}
|
||||
if cfg.KbDbPassword == "shared_kb_secret_pass" {
|
||||
t.Fatal("must not use obsolete insecure default password")
|
||||
}
|
||||
wantHost := "127.0.0.1"
|
||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||
wantHost = "shuimu-kb-shared"
|
||||
}
|
||||
if cfg.KbDbHost != wantHost {
|
||||
t.Fatalf("expected host normalize to %s, got %s", wantHost, cfg.KbDbHost)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeKBHostForRuntime(t *testing.T) {
|
||||
cfg := &config.Config{KbDbHost: "shuimu-kb-shared"}
|
||||
config.NormalizeKBHostForRuntime(cfg)
|
||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||
if cfg.KbDbHost != "shuimu-kb-shared" {
|
||||
t.Fatalf("in docker must keep compose host, got %s", cfg.KbDbHost)
|
||||
}
|
||||
return
|
||||
}
|
||||
if cfg.KbDbHost != "127.0.0.1" {
|
||||
t.Fatalf("on host expected 127.0.0.1, got %s", cfg.KbDbHost)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactPostgresDSN(t *testing.T) {
|
||||
got := config.RedactPostgresDSN("postgres://regkb:super-secret@127.0.0.1:5432/regulations?sslmode=disable")
|
||||
if got != "postgres://regkb:***@127.0.0.1:5432/regulations?sslmode=disable" {
|
||||
t.Fatalf("unexpected redact: %s", got)
|
||||
}
|
||||
if strings.Contains(got, "super-secret") {
|
||||
t.Fatal("password leaked in redacted DSN")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveLayeredLoadHasNonDefaultPassword(t *testing.T) {
|
||||
if os.Getenv("CHECK_LIVE_KB_ENV") != "1" {
|
||||
t.Skip("set CHECK_LIVE_KB_ENV=1 to assert workspace root .env")
|
||||
}
|
||||
cfg, err := config.Load("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.KbDbPassword == "" || cfg.KbDbPassword == "shared_kb_secret_pass" {
|
||||
t.Fatalf("live Load missing real KB password (len=%d)", len(cfg.KbDbPassword))
|
||||
}
|
||||
if cfg.KbDbHost != "127.0.0.1" && cfg.KbDbHost != "shuimu-kb-shared" {
|
||||
t.Fatalf("unexpected host %s", cfg.KbDbHost)
|
||||
}
|
||||
t.Logf("live kb host=%s port=%d password_set=true", cfg.KbDbHost, cfg.KbDbPort)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user