feat: accept ND_-prefixed env var names in config files (#5258)

* feat: add toPascalCase helper for config key display

Adds a toPascalCase helper that converts dotted lowercase Viper config keys
(e.g. 'scanner.schedule') to PascalCase (e.g. 'Scanner.Schedule') for use
in user-facing warning messages. Includes export_test.go binding and a
full Ginkgo DescribeTable test suite covering simple, dotted, multi-segment,
already-capitalized, and empty-string cases.

* feat: remap ND_-prefixed env var names found in config files

Detect when users mistakenly use environment variable names (like
ND_ADDRESS) in config files, remap them to canonical keys, and warn.
Fatal error if both ND_ and canonical versions of the same key exist.

Closes #5242
This commit is contained in:
Deluan Quintão
2026-03-28 13:17:31 -04:00
committed by GitHub
parent 2588558946
commit 2b041c02ad
5 changed files with 121 additions and 0 deletions
+56
View File
@@ -108,6 +108,62 @@ var _ = Describe("Configuration", func() {
Entry("falls back to 'fts' for empty string", "", "fts"),
)
DescribeTable("ToPascalCase",
func(input, expected string) {
Expect(conf.ToPascalCase(input)).To(Equal(expected))
},
Entry("simple key", "address", "Address"),
Entry("dotted key", "scanner.schedule", "Scanner.Schedule"),
Entry("already capitalized", "Address", "Address"),
Entry("multi-segment", "lastfm.enabled", "Lastfm.Enabled"),
Entry("empty string", "", ""),
)
Describe("remapEnvVarKeysFromConfig", func() {
BeforeEach(func() {
viper.Reset()
conf.SetViperDefaults()
viper.SetDefault("datafolder", GinkgoT().TempDir())
viper.SetDefault("loglevel", "error")
conf.ResetConf()
})
It("remaps ND_-prefixed keys to canonical keys", func() {
filename := filepath.Join("testdata", "cfg_nd_keys.toml")
conf.InitConfig(filename, false)
conf.Load(true)
Expect(conf.Server.Address).To(Equal("127.0.0.1"))
Expect(conf.Server.Port).To(Equal(4531))
Expect(conf.Server.Scanner.Schedule).To(Equal("@every 1h"))
})
It("exits with fatal error when both ND_ and canonical key exist", func() {
cleanup := conf.SetFatalFunc(func(msg string) {
panic(msg)
})
defer cleanup()
filename := filepath.Join("testdata", "cfg_nd_conflict.toml")
conf.InitConfig(filename, false)
Expect(func() { conf.Load(true) }).To(PanicWith(And(
ContainSubstring("ND_ADDRESS"),
ContainSubstring("Address"),
ContainSubstring("only needed for environment variables"),
)))
})
It("does nothing when no ND_ keys are present", func() {
filename := filepath.Join("testdata", "cfg.toml")
conf.InitConfig(filename, false)
conf.Load(true)
// Verify normal config loading still works
Expect(conf.Server.MusicFolder).To(Equal("/toml/music"))
})
})
DescribeTable("should load configuration from",
func(format string) {
filename := filepath.Join("testdata", "cfg."+format)