package config import ( "os" "path/filepath" "strings" "testing" ) // A real bcrypt hash for "correct horse" — full of `$word` sequences ($2a, $10, and a $-laden salt) // that os.ExpandEnv would each replace with an (empty) env value, silently corrupting the hash. const bcryptHash = `$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy` func minimalYAML(passwordHash string) string { return `customer: id: demo domain: demo.example.com web: password_hash: "` + passwordHash + `" ` } // TestLoadFromBytes_BcryptHashIntact is the F-C2-1 headline red-proof: a bcrypt password_hash must // survive config load BYTE-IDENTICAL. On the pre-fix code (os.ExpandEnv over the whole file) the // `$2a$`/`$10$`/`$…salt` segments get expanded to empty → the stored hash is corrupted → silent // auth breakage. This test FAILS on that pre-fix path. func TestLoadFromBytes_BcryptHashIntact(t *testing.T) { // Make the corruption visible if ExpandEnv ever creeps back: set env vars matching hash segments. t.Setenv("2a", "CORRUPT") t.Setenv("10", "CORRUPT") cfg, err := LoadFromBytes([]byte(minimalYAML(bcryptHash))) if err != nil { t.Fatalf("LoadFromBytes: %v", err) } if cfg.Web.PasswordHash != bcryptHash { t.Fatalf("password_hash corrupted on load:\n want %q\n got %q", bcryptHash, cfg.Web.PasswordHash) } } // TestLoadAndParse_BcryptHashIntact proves the same for the file path (loadAndParse / Load). func TestLoadAndParse_BcryptHashIntact(t *testing.T) { t.Setenv("2a", "CORRUPT") dir := t.TempDir() p := filepath.Join(dir, "controller.yaml") if err := os.WriteFile(p, []byte(minimalYAML(bcryptHash)), 0o600); err != nil { t.Fatal(err) } cfg, err := loadAndParse(p) if err != nil { t.Fatalf("loadAndParse: %v", err) } if cfg.Web.PasswordHash != bcryptHash { t.Fatalf("password_hash corrupted on file load:\n want %q\n got %q", bcryptHash, cfg.Web.PasswordHash) } } // TestEnvOverride_PasswordHash proves the sanctioned typed env path still wins (applyEnvOverrides). func TestEnvOverride_PasswordHash(t *testing.T) { override := `$2a$10$differentHASHvalueForOverrideTestXXXXXXXXXXXXXXXXXXXXXXXX` t.Setenv("FELHOM_WEB_PASSWORD_HASH", override) cfg, err := LoadFromBytes([]byte(minimalYAML(bcryptHash))) if err != nil { t.Fatalf("LoadFromBytes: %v", err) } if cfg.Web.PasswordHash != override { t.Fatalf("FELHOM_WEB_PASSWORD_HASH override not applied:\n want %q\n got %q", override, cfg.Web.PasswordHash) } } // TestLiteralDollarVarPreserved documents the behavior change: a literal ${VAR} in a value is now // kept verbatim (no file-level expansion). Session secrets or comments with `$` survive intact. func TestLiteralDollarVarPreserved(t *testing.T) { y := `customer: id: demo domain: demo.example.com web: session_secret: "literal-${NOT_EXPANDED}-value" ` cfg, err := LoadFromBytes([]byte(y)) if err != nil { t.Fatalf("LoadFromBytes: %v", err) } if !strings.Contains(cfg.Web.SessionSecret, "${NOT_EXPANDED}") { t.Fatalf("literal ${VAR} was expanded (should be preserved): %q", cfg.Web.SessionSecret) } }