mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-07 15:31:30 +02:00
[management,signal,proxy,relay,misc] Increase test surface for configuration backwards compatibility
The shared loader changes how each service reads environment variables, flags and configuration files. These tests assert the behavior main produced for concrete inputs (env names and aliases, empty and invalid values, list parsing, flag precedence, file decoding shapes, key persistence), so every intentional break shows up as a failing test that can be reviewed and either restored or documented.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,20 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExampleConfig(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
require.NoError(t, rootCmd.ParseFlags(nil))
|
||||
oldConfigPath := configPath
|
||||
configPath = filepath.Join("..", "config.example.yaml")
|
||||
@@ -23,7 +28,168 @@ func TestExampleConfig(t *testing.T) {
|
||||
assert.True(t, cfg.EnableSTUN, "Example config should enable STUN")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEffectiveDefaults(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
cfg.LetsencryptDomains = nil
|
||||
assert.Equal(t, defaultConfig(), cfg, "Relay effective defaults should remain unchanged")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvironmentBindings(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_LISTEN_ADDRESS", ":7443")
|
||||
t.Setenv("NB_EXPOSED_ADDRESS", "rels://relay.example.com:443")
|
||||
t.Setenv("NB_METRICS_PORT", "9191")
|
||||
t.Setenv("NB_LETSENCRYPT_EMAIL", "admin@example.com")
|
||||
t.Setenv("NB_LETSENCRYPT_DATA_DIR", "/var/lib/relay/certs")
|
||||
t.Setenv("NB_LETSENCRYPT_DOMAINS", "relay.example.com,relay-alt.example.com")
|
||||
t.Setenv("NB_LETSENCRYPT_AWS_ROUTE53", "true")
|
||||
t.Setenv("NB_TLS_CERT_FILE", "/etc/relay/tls.crt")
|
||||
t.Setenv("NB_TLS_KEY_FILE", "/etc/relay/tls.key")
|
||||
t.Setenv("NB_AUTH_SECRET", "relay-secret")
|
||||
t.Setenv("NB_LOG_LEVEL", "debug")
|
||||
t.Setenv("NB_LOG_FILE", "/var/log/relay.log")
|
||||
t.Setenv("NB_HEALTH_LISTEN_ADDRESS", ":9001")
|
||||
t.Setenv("NB_TRUSTED_PROXIES", "192.0.2.0/24")
|
||||
t.Setenv("NB_ENABLE_STUN", "true")
|
||||
t.Setenv("NB_STUN_PORTS", "3479,3480")
|
||||
t.Setenv("NB_STUN_LOG_LEVEL", "trace")
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &Config{
|
||||
ListenAddress: ":7443",
|
||||
ExposedAddress: "rels://relay.example.com:443",
|
||||
MetricsPort: 9191,
|
||||
LetsencryptEmail: "admin@example.com",
|
||||
LetsencryptDataDir: "/var/lib/relay/certs",
|
||||
LetsencryptDomains: []string{"relay.example.com", "relay-alt.example.com"},
|
||||
LetsencryptAWSRoute53: true,
|
||||
TlsCertFile: "/etc/relay/tls.crt",
|
||||
TlsKeyFile: "/etc/relay/tls.key",
|
||||
AuthSecret: "relay-secret",
|
||||
LogLevel: "debug",
|
||||
LogFile: "/var/log/relay.log",
|
||||
HealthcheckListenAddress: ":9001",
|
||||
TrustedProxies: "192.0.2.0/24",
|
||||
EnableSTUN: true,
|
||||
STUNPorts: []int{3479, 3480},
|
||||
STUNLogLevel: "trace",
|
||||
}, cfg, "Every legacy Relay environment binding should remain supported")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvironmentParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
envName string
|
||||
value string
|
||||
validate func(*testing.T, *Config)
|
||||
}{
|
||||
{
|
||||
name: "invalid integer becomes zero",
|
||||
envName: "NB_METRICS_PORT",
|
||||
value: "invalid",
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Zero(t, cfg.MetricsPort, "Invalid legacy integer input should retain its parsed zero value")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy boolean spelling remains false",
|
||||
envName: "NB_ENABLE_STUN",
|
||||
value: "yes",
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.False(t, cfg.EnableSTUN, "Unsupported legacy boolean spelling should remain disabled")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid boolean remains false",
|
||||
envName: "NB_ENABLE_STUN",
|
||||
value: "invalid",
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.False(t, cfg.EnableSTUN, "Invalid legacy boolean input should retain its default")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty integer list retains default",
|
||||
envName: "NB_STUN_PORTS",
|
||||
value: "",
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, []int{3478}, cfg.STUNPorts, "Empty legacy integer lists should retain their default")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trailing comma integer list retains default",
|
||||
envName: "NB_STUN_PORTS",
|
||||
value: "3479,",
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, []int{3478}, cfg.STUNPorts, "Invalid legacy integer lists should retain their default")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "quoted comma in string list",
|
||||
envName: "NB_LETSENCRYPT_DOMAINS",
|
||||
value: `"relay,one.example.com",relay-two.example.com`,
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, []string{"relay,one.example.com", "relay-two.example.com"}, cfg.LetsencryptDomains,
|
||||
"Legacy string lists should retain CSV quoting")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "malformed quoted string list is ignored",
|
||||
envName: "NB_LETSENCRYPT_DOMAINS",
|
||||
value: `"relay.example.com`,
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Nil(t, cfg.LetsencryptDomains, "Malformed legacy string lists should retain their default")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv(test.envName, test.value)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
if !assert.NoError(t, err, "Legacy environment parse errors should not abort Relay startup") {
|
||||
return
|
||||
}
|
||||
test.validate(t, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyRelayFlagsRemainRegistered(t *testing.T) {
|
||||
expected := map[string]string{
|
||||
"listen-address": "l",
|
||||
"exposed-address": "e",
|
||||
"metrics-port": "",
|
||||
"letsencrypt-data-dir": "d",
|
||||
"letsencrypt-domains": "a",
|
||||
"letsencrypt-email": "",
|
||||
"letsencrypt-aws-route53": "",
|
||||
"tls-cert-file": "c",
|
||||
"tls-key-file": "k",
|
||||
"auth-secret": "s",
|
||||
"log-level": "",
|
||||
"log-file": "",
|
||||
"health-listen-address": "H",
|
||||
"trusted-proxies": "",
|
||||
"enable-stun": "",
|
||||
"stun-ports": "",
|
||||
"stun-log-level": "",
|
||||
}
|
||||
|
||||
for name, shorthand := range expected {
|
||||
flag := rootCmd.Flags().Lookup(name)
|
||||
require.NotNil(t, flag, "Legacy flag %s should remain registered", name)
|
||||
assert.Equal(t, shorthand, flag.Shorthand, "Legacy shorthand for %s should remain unchanged", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPrecedence(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
path := filepath.Join(t.TempDir(), "relay.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`
|
||||
listenAddress: ":8443"
|
||||
@@ -60,3 +226,476 @@ stunPorts: [3478, 3479]
|
||||
assert.True(t, cfg.EnableSTUN, "STUN should be configurable from the file")
|
||||
assert.Equal(t, []int{3478, 3479}, cfg.STUNPorts, "STUN ports should load from the file")
|
||||
}
|
||||
|
||||
func loadRelayConfigWithoutFile(t *testing.T) (*Config, error) {
|
||||
t.Helper()
|
||||
|
||||
require.NoError(t, rootCmd.ParseFlags(nil))
|
||||
oldConfigPath := configPath
|
||||
configPath = ""
|
||||
t.Cleanup(func() {
|
||||
configPath = oldConfigPath
|
||||
})
|
||||
return loadConfig(rootCmd)
|
||||
}
|
||||
|
||||
func clearRelayConfigEnvironment(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for _, name := range []string{
|
||||
"NB_LISTEN_ADDRESS",
|
||||
"NB_EXPOSED_ADDRESS",
|
||||
"NB_METRICS_PORT",
|
||||
"NB_LETSENCRYPT_EMAIL",
|
||||
"NB_LETSENCRYPT_DATA_DIR",
|
||||
"NB_LETSENCRYPT_DOMAINS",
|
||||
"NB_LETSENCRYPT_AWS_ROUTE53",
|
||||
"NB_TLS_CERT_FILE",
|
||||
"NB_TLS_KEY_FILE",
|
||||
"NB_AUTH_SECRET",
|
||||
"NB_LOG_LEVEL",
|
||||
"NB_LOG_FILE",
|
||||
"NB_HEALTH_LISTEN_ADDRESS",
|
||||
"NB_TRUSTED_PROXIES",
|
||||
"NB_ENABLE_STUN",
|
||||
"NB_STUN_PORTS",
|
||||
"NB_STUN_LOG_LEVEL",
|
||||
} {
|
||||
t.Setenv(name, "")
|
||||
require.NoError(t, os.Unsetenv(name))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyIntegerOverflowParsing(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_METRICS_PORT", "99999999999999999999")
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
if !assert.NoError(t, err, "Legacy pflag stored the clamped integer and only logged the range error, so loading continued") {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, math.MaxInt64, cfg.MetricsPort,
|
||||
"Legacy strconv.ParseInt range errors left math.MaxInt64 in the metrics port instead of aborting startup")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyRoute53BooleanSpelling(t *testing.T) {
|
||||
for _, value := range []string{"yes", "y", "on", "YES", "On"} {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_LETSENCRYPT_AWS_ROUTE53", value)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
if !assert.NoError(t, err, "Legacy boolean parse errors should not abort Relay startup") {
|
||||
return
|
||||
}
|
||||
assert.False(t, cfg.LetsencryptAWSRoute53,
|
||||
"Legacy strconv.ParseBool rejected %q, leaving Route 53 disabled so the Route 53 TLS branch was never taken", value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyRoute53InvalidBoolean(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "invalid word", value: "invalid"},
|
||||
{name: "numeric two", value: "2"},
|
||||
{name: "mixed case true", value: "tRuE"},
|
||||
{name: "trailing space", value: "true "},
|
||||
{name: "enabled", value: "enabled"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_LETSENCRYPT_AWS_ROUTE53", test.value)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
if !assert.NoError(t, err, "Legacy invalid boolean input was logged at Info and ignored, not fatal") {
|
||||
return
|
||||
}
|
||||
assert.False(t, cfg.LetsencryptAWSRoute53,
|
||||
"Legacy strconv.ParseBool rejected %q and pflag stored false, so Relay started with Route 53 disabled", test.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacySTUNPortListRejection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "non-numeric trailing element", value: "3479,abc"},
|
||||
{name: "non-numeric single element", value: "abc"},
|
||||
{name: "space after comma", value: "3479, 3480"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_STUN_PORTS", test.value)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
if !assert.NoError(t, err, "Legacy integer list parse errors were logged at Info and ignored, not fatal") {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, []int{3478}, cfg.STUNPorts,
|
||||
"Legacy pflag intSlice ran strconv.Atoi on every element and rejected the whole value %q, retaining the default", test.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacySTUNPortDecimalParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
expected []int
|
||||
}{
|
||||
{name: "leading zero", value: "010", expected: []int{10}},
|
||||
{name: "double leading zero", value: "0010", expected: []int{10}},
|
||||
{name: "leading zero element", value: "3478,010", expected: []int{3478, 10}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_STUN_PORTS", test.value)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, cfg.STUNPorts,
|
||||
"Legacy pflag intSlice used strconv.Atoi (base 10 only), so leading zeros were decimal and never octal")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacySTUNPortStrictDecimalSyntax(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "hex prefix", value: "0x10"},
|
||||
{name: "underscore separator", value: "3_479"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_STUN_PORTS", test.value)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
if !assert.NoError(t, err, "Legacy integer list parse errors were logged at Info and ignored, not fatal") {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, []int{3478}, cfg.STUNPorts,
|
||||
"Legacy strconv.Atoi rejected %q (no hex prefix or underscores), so the default STUN port list was retained", test.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyNewlineInDomainList(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_LETSENCRYPT_DOMAINS", "a.example.com\nb.example.com")
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"a.example.com"}, cfg.LetsencryptDomains,
|
||||
"Legacy pflag readAsCSV performed a single csv.Reader.Read, so only the first line of a newline separated value was used")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvAndFlagDomainAppend(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flagValues []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "single flag value",
|
||||
flagValues: []string{"b.example.com"},
|
||||
expected: []string{"a.example.com", "b.example.com"},
|
||||
},
|
||||
{
|
||||
name: "repeated flag values",
|
||||
flagValues: []string{"b.example.com", "c.example.com"},
|
||||
expected: []string{"a.example.com", "b.example.com", "c.example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_LETSENCRYPT_DOMAINS", "a.example.com")
|
||||
setRelaySliceFlag(t, "letsencrypt-domains", test.flagValues)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, cfg.LetsencryptDomains,
|
||||
"Legacy applied NB_LETSENCRYPT_DOMAINS via flags.Set before argv parsing, so command line domains were appended to the environment list")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvAndFlagSTUNPortAppend(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_STUN_PORTS", "3479")
|
||||
setRelaySliceFlag(t, "stun-ports", []string{"3480"})
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{3479, 3480}, cfg.STUNPorts,
|
||||
"Legacy applied NB_STUN_PORTS via flags.Set before argv parsing, so --stun-ports appended to the environment list and both UDP ports were bound")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyDuplicateSTUNPortFromEnvAndFlag(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_EXPOSED_ADDRESS", "rels://relay.example.com:443")
|
||||
t.Setenv("NB_AUTH_SECRET", "relay-secret")
|
||||
t.Setenv("NB_ENABLE_STUN", "true")
|
||||
t.Setenv("NB_STUN_PORTS", "3478")
|
||||
setRelaySliceFlag(t, "stun-ports", []string{"3478"})
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []int{3478, 3478}, cfg.STUNPorts,
|
||||
"Legacy appended the --stun-ports value to the NB_STUN_PORTS value, producing a duplicated port list")
|
||||
|
||||
err = cfg.Validate()
|
||||
if assert.Error(t, err, "Legacy Validate rejected the duplicated STUN port list and Relay exited") {
|
||||
assert.Contains(t, err.Error(), "duplicate STUN port 3478", "Legacy reported the duplicate STUN port")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvironmentNameMatching(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
validate func(*testing.T, *Config)
|
||||
}{
|
||||
{
|
||||
name: "listen address alias does not outrank legacy name",
|
||||
env: map[string]string{"NB_LISTENADDRESS": ":1", "NB_LISTEN_ADDRESS": ":2"},
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, ":2", cfg.ListenAddress,
|
||||
"Legacy only read NB_LISTEN_ADDRESS; the un-underscored NB_LISTENADDRESS was ignored")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "healthcheck alias is ignored",
|
||||
env: map[string]string{"NB_HEALTHCHECKLISTENADDRESS": ":1"},
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, ":9000", cfg.HealthcheckListenAddress,
|
||||
"Legacy only read NB_HEALTH_LISTEN_ADDRESS; NB_HEALTHCHECKLISTENADDRESS was ignored and the default retained")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stun ports alias is ignored",
|
||||
env: map[string]string{"NB_STUNPORTS": "1111"},
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, []int{3478}, cfg.STUNPorts,
|
||||
"Legacy only read NB_STUN_PORTS; NB_STUNPORTS was ignored and the default retained")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "auth secret alias is ignored",
|
||||
env: map[string]string{"NB_AUTHSECRET": "alias-secret"},
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Empty(t, cfg.AuthSecret,
|
||||
"Legacy only read NB_AUTH_SECRET; NB_AUTHSECRET was ignored")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
for name, value := range test.env {
|
||||
t.Setenv(name, value)
|
||||
}
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
test.validate(t, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyValuesWithEmptyEnvironmentAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
validate func(*testing.T, *Config)
|
||||
}{
|
||||
{
|
||||
name: "empty log level alias keeps default",
|
||||
env: map[string]string{"NB_LOGLEVEL": ""},
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, "info", cfg.LogLevel,
|
||||
"Legacy never read NB_LOGLEVEL, so an empty alias could not blank the log level and break InitLog")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty listen address alias keeps legacy name",
|
||||
env: map[string]string{"NB_LISTENADDRESS": "", "NB_LISTEN_ADDRESS": ":2"},
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, ":2", cfg.ListenAddress,
|
||||
"Legacy never read NB_LISTENADDRESS, so NB_LISTEN_ADDRESS remained effective")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty healthcheck alias keeps legacy name",
|
||||
env: map[string]string{"NB_HEALTHCHECKLISTENADDRESS": "", "NB_HEALTH_LISTEN_ADDRESS": ":1"},
|
||||
validate: func(t *testing.T, cfg *Config) {
|
||||
assert.Equal(t, ":1", cfg.HealthcheckListenAddress,
|
||||
"Legacy never read NB_HEALTHCHECKLISTENADDRESS, so NB_HEALTH_LISTEN_ADDRESS remained effective")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
for name, value := range test.env {
|
||||
t.Setenv(name, value)
|
||||
}
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
test.validate(t, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyRelayHasNoConfigFlag(t *testing.T) {
|
||||
require.NoError(t, rootCmd.ParseFlags(nil))
|
||||
|
||||
configFlag := rootCmd.Flags().Lookup("config")
|
||||
assert.Nil(t, configFlag, "Legacy Relay registered no --config flag, so --help did not list it")
|
||||
|
||||
if configFlag != nil {
|
||||
oldValue := configFlag.Value.String()
|
||||
oldChanged := configFlag.Changed
|
||||
oldConfigPath := configPath
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, configFlag.Value.Set(oldValue))
|
||||
configFlag.Changed = oldChanged
|
||||
configPath = oldConfigPath
|
||||
})
|
||||
}
|
||||
|
||||
err := rootCmd.ParseFlags([]string{"--config", filepath.Join(t.TempDir(), "relay.yaml")})
|
||||
if assert.Error(t, err, "Legacy Relay rejected --config on the command line and exited with 'failed to execute command'") {
|
||||
assert.Contains(t, err.Error(), "unknown flag: --config", "Legacy cobra reported --config as an unknown flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyNBConfigIgnored(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
path := filepath.Join(t.TempDir(), "relay.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte("authSecret: file-secret\n"), 0o600))
|
||||
t.Setenv("NB_CONFIG", path)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, cfg.AuthSecret, "Legacy Relay never read NB_CONFIG, so no configuration file was loaded from it")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyNilDomainsDefault(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, cfg.LetsencryptDomains,
|
||||
"Legacy left LetsencryptDomains nil when neither NB_LETSENCRYPT_DOMAINS nor --letsencrypt-domains was given, and Route53TLS.Domains received nil")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvironmentParseDiagnostics(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_METRICS_PORT", "abc")
|
||||
|
||||
logger := log.StandardLogger()
|
||||
oldOutput := logger.Out
|
||||
oldLevel := logger.GetLevel()
|
||||
var logs bytes.Buffer
|
||||
logger.SetOutput(&logs)
|
||||
logger.SetLevel(log.TraceLevel)
|
||||
t.Cleanup(func() {
|
||||
logger.SetOutput(oldOutput)
|
||||
logger.SetLevel(oldLevel)
|
||||
})
|
||||
|
||||
_, err := loadRelayConfigWithoutFile(t)
|
||||
assert.NoError(t, err, "Legacy rejected environment values were logged and ignored; startup continued")
|
||||
assert.Contains(t, logs.String(), "unable to configure flag metrics-port using variable NB_METRICS_PORT",
|
||||
"Legacy emitted an Info diagnostic naming the flag and the NB_ variable when an environment value was rejected")
|
||||
}
|
||||
|
||||
func setRelaySliceFlag(t *testing.T, name string, values []string) {
|
||||
t.Helper()
|
||||
|
||||
require.NoError(t, rootCmd.ParseFlags(nil))
|
||||
flag := rootCmd.Flags().Lookup(name)
|
||||
require.NotNil(t, flag, "flag %s should be registered", name)
|
||||
sliceValue, ok := flag.Value.(pflag.SliceValue)
|
||||
require.True(t, ok, "flag %s should be a slice flag", name)
|
||||
|
||||
oldValues := sliceValue.GetSlice()
|
||||
oldChanged := flag.Changed
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, sliceValue.Replace(oldValues))
|
||||
flag.Changed = oldChanged
|
||||
})
|
||||
require.NoError(t, sliceValue.Replace(values))
|
||||
flag.Changed = true
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyQuotedDomainListElements(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
expected []string
|
||||
}{
|
||||
{name: "single quoted element", value: `"relay.example.com"`, expected: []string{"relay.example.com"}},
|
||||
{name: "quoted trailing element", value: `a.example.com,"b.example.com"`, expected: []string{"a.example.com", "b.example.com"}},
|
||||
{name: "csv escaped double quote", value: `"a""b"`, expected: []string{`a"b`}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_LETSENCRYPT_DATA_DIR", "/var/lib/relay/certs")
|
||||
t.Setenv("NB_LETSENCRYPT_DOMAINS", test.value)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, cfg.LetsencryptDomains,
|
||||
"Legacy pflag readAsCSV used encoding/csv, which stripped surrounding quotes and unescaped doubled quotes in %q, so the Let's Encrypt host whitelist held the clean hostname", test.value)
|
||||
assert.True(t, cfg.HasLetsEncrypt(), "Legacy still entered the Let's Encrypt path for a quoted domain list")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyTrailingNewlineInDomainList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
expected []string
|
||||
}{
|
||||
{name: "single domain", value: "relay.example.com\n", expected: []string{"relay.example.com"}},
|
||||
{name: "multiple domains", value: "a.example.com,b.example.com\n", expected: []string{"a.example.com", "b.example.com"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearRelayConfigEnvironment(t)
|
||||
t.Setenv("NB_LETSENCRYPT_DATA_DIR", "/var/lib/relay/certs")
|
||||
t.Setenv("NB_LETSENCRYPT_DOMAINS", test.value)
|
||||
|
||||
cfg, err := loadRelayConfigWithoutFile(t)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, cfg.LetsencryptDomains,
|
||||
"Legacy pflag readAsCSV let csv.Reader.Read consume the trailing newline in %q as the record terminator, so the Let's Encrypt host whitelist held the clean hostname without a newline", test.value)
|
||||
assert.True(t, cfg.HasLetsEncrypt(), "Legacy still entered the Let's Encrypt path for a newline terminated domain list")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,186 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExampleConfig(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
cfg, err := loadConfig(runCmd, filepath.Join("..", "config.example.yaml"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/etc/netbird/tls.crt", cfg.CertFile, "Example config should load")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyDefaults(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaultConfig(), cfg, "Signal defaults should remain unchanged")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvironmentBindings(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
t.Setenv("NB_PORT", "10001")
|
||||
t.Setenv("NB_LETSENCRYPT_DOMAIN", "signal.example.com")
|
||||
t.Setenv("NB_LETSENCRYPT_EMAIL", "admin@example.com")
|
||||
t.Setenv("NB_LETSENCRYPT_DATA_DIR", "/var/lib/signal/certs")
|
||||
t.Setenv("NB_CERT_FILE", "/etc/signal/tls.crt")
|
||||
t.Setenv("NB_CERT_KEY", "/etc/signal/tls.key")
|
||||
t.Setenv("NB_PPROF_ADDR", "localhost:6060")
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &Config{
|
||||
Port: 10001,
|
||||
MetricsPort: 9090,
|
||||
LetsencryptDomain: "signal.example.com",
|
||||
LetsencryptEmail: "admin@example.com",
|
||||
LetsencryptDataDir: "/var/lib/signal/certs",
|
||||
CertFile: "/etc/signal/tls.crt",
|
||||
CertKey: "/etc/signal/tls.key",
|
||||
LogLevel: "info",
|
||||
LogFile: defaultConfig().LogFile,
|
||||
PprofAddress: "localhost:6060",
|
||||
}, cfg, "Every legacy Signal environment binding should remain supported")
|
||||
}
|
||||
|
||||
func TestLoadConfigIgnoresPreviouslyUnboundEmptyEnvironment(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
t.Setenv("NB_METRICS_PORT", "")
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaultConfig().MetricsPort, cfg.MetricsPort,
|
||||
"An environment variable that was previously unbound should not alter the default when empty")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvironmentAliasPrecedence(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
t.Setenv("NB_LETSENCRYPT_DATA_DIR", "/preferred-certs")
|
||||
t.Setenv("NB_SSL_DIR", "/legacy-certs")
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/legacy-certs", cfg.LetsencryptDataDir,
|
||||
"The legacy alias should retain its previous precedence when both variables are set")
|
||||
}
|
||||
|
||||
func TestSignalPortDefaultsRemainCompatible(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
expected int
|
||||
}{
|
||||
{name: "no TLS", expected: 80},
|
||||
{name: "letsencrypt", env: map[string]string{"NB_LETSENCRYPT_DOMAIN": "signal.example.com"}, expected: 443},
|
||||
{name: "certificate pair", env: map[string]string{"NB_CERT_FILE": "/tls.crt", "NB_CERT_KEY": "/tls.key"}, expected: 443},
|
||||
{name: "explicit nonzero port", env: map[string]string{"NB_PORT": "10002"}, expected: 10002},
|
||||
{name: "explicit zero port", env: map[string]string{"NB_PORT": "0"}, expected: 0},
|
||||
{name: "invalid port falls back", env: map[string]string{"NB_PORT": "invalid"}, expected: 80},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := runSignalPreRun(t, test.env)
|
||||
assert.Equal(t, test.expected, actual, "Signal implicit port selection should retain legacy behavior")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignalFlagAliasesRetainArgumentOrder(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
first string
|
||||
second string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "legacy alias last",
|
||||
first: "letsencrypt-data-dir",
|
||||
second: "ssl-dir",
|
||||
expected: "/ssl-dir",
|
||||
},
|
||||
{
|
||||
name: "preferred alias last",
|
||||
first: "ssl-dir",
|
||||
second: "letsencrypt-data-dir",
|
||||
expected: "/letsencrypt-data-dir",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
firstFlag := runCmd.PersistentFlags().Lookup(test.first)
|
||||
secondFlag := runCmd.PersistentFlags().Lookup(test.second)
|
||||
require.NotNil(t, firstFlag, "First alias should be registered")
|
||||
require.NotNil(t, secondFlag, "Second alias should be registered")
|
||||
oldDataDir := signalLetsencryptDataDir
|
||||
oldFirstValue, oldFirstChanged := firstFlag.Value.String(), firstFlag.Changed
|
||||
oldSecondValue, oldSecondChanged := secondFlag.Value.String(), secondFlag.Changed
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, firstFlag.Value.Set(oldFirstValue))
|
||||
firstFlag.Changed = oldFirstChanged
|
||||
require.NoError(t, secondFlag.Value.Set(oldSecondValue))
|
||||
secondFlag.Changed = oldSecondChanged
|
||||
signalLetsencryptDataDir = oldDataDir
|
||||
})
|
||||
|
||||
require.NoError(t, firstFlag.Value.Set("/"+test.first))
|
||||
firstFlag.Changed = true
|
||||
require.NoError(t, secondFlag.Value.Set("/"+test.second))
|
||||
secondFlag.Changed = true
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, cfg.LetsencryptDataDir,
|
||||
"When both CLI aliases are supplied, the last value should retain precedence")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacySignalFlagsRemainRegistered(t *testing.T) {
|
||||
for _, name := range []string{
|
||||
"port",
|
||||
"letsencrypt-data-dir",
|
||||
"ssl-dir",
|
||||
"letsencrypt-domain",
|
||||
"letsencrypt-email",
|
||||
"cert-file",
|
||||
"cert-key",
|
||||
} {
|
||||
flag := runCmd.PersistentFlags().Lookup(name)
|
||||
require.NotNil(t, flag, "Legacy persistent flag %s should remain registered", name)
|
||||
assert.Empty(t, flag.Shorthand, "Legacy Signal flag %s should remain without a shorthand", name)
|
||||
}
|
||||
metricsFlag := runCmd.Flags().Lookup("metrics-port")
|
||||
require.NotNil(t, metricsFlag, "Legacy metrics flag should remain registered")
|
||||
assert.Empty(t, metricsFlag.Shorthand, "Legacy metrics flag should remain without a shorthand")
|
||||
for _, name := range []string{"log-level", "log-file"} {
|
||||
flag := rootCmd.PersistentFlags().Lookup(name)
|
||||
require.NotNil(t, flag, "Legacy root flag %s should remain registered", name)
|
||||
assert.Empty(t, flag.Shorthand, "Legacy Signal flag %s should remain without a shorthand", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPrecedence(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
configPath := filepath.Join(t.TempDir(), "signal.yaml")
|
||||
require.NoError(t, os.WriteFile(configPath, []byte(`
|
||||
port: 10001
|
||||
@@ -28,7 +192,7 @@ pprofAddress: localhost:6060
|
||||
t.Setenv("NB_SSL_DIR", "/legacy-certs")
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
portFlag := runCmd.Flags().Lookup("port")
|
||||
portFlag := runCmd.PersistentFlags().Lookup("port")
|
||||
oldPort := portFlag.Value.String()
|
||||
oldChanged := portFlag.Changed
|
||||
t.Cleanup(func() {
|
||||
@@ -46,3 +210,392 @@ pprofAddress: localhost:6060
|
||||
assert.Equal(t, "/legacy-certs", cfg.LetsencryptDataDir, "Legacy environment aliases should remain supported")
|
||||
assert.Equal(t, "localhost:6060", cfg.PprofAddress, "Environment-only settings should load from the file")
|
||||
}
|
||||
|
||||
func runSignalPreRun(t *testing.T, environment map[string]string) int {
|
||||
t.Helper()
|
||||
|
||||
clearSignalConfigEnvironment(t)
|
||||
for name, value := range environment {
|
||||
t.Setenv(name, value)
|
||||
}
|
||||
t.Setenv("NB_LOG_FILE", "console")
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
require.NoError(t, executeSignalPreRun(t, "0", false))
|
||||
return signalPort
|
||||
}
|
||||
|
||||
// executeSignalPreRun snapshots the Signal runtime globals, the port flag and the standard logger,
|
||||
// forces the port flag into the requested state, runs runCmd.PreRunE against the current
|
||||
// environment and returns its error. Callers prepare the environment and call ParseFlags first.
|
||||
func executeSignalPreRun(t *testing.T, portValue string, portChanged bool) error {
|
||||
t.Helper()
|
||||
|
||||
oldSignalPort := signalPort
|
||||
oldMetricsPort := metricsPort
|
||||
oldLetsencryptDomain := signalLetsencryptDomain
|
||||
oldLetsencryptEmail := signalLetsencryptEmail
|
||||
oldLetsencryptDataDir := signalLetsencryptDataDir
|
||||
oldCertFile := signalCertFile
|
||||
oldCertKey := signalCertKey
|
||||
oldLogLevel := logLevel
|
||||
oldLogFile := logFile
|
||||
oldPprofAddress := signalPprofAddress
|
||||
oldConfigPath := signalConfigPath
|
||||
portFlag := runCmd.PersistentFlags().Lookup("port")
|
||||
require.NotNil(t, portFlag, "Signal port flag should be registered")
|
||||
oldPortValue := portFlag.Value.String()
|
||||
oldPortChanged := portFlag.Changed
|
||||
logger := log.StandardLogger()
|
||||
oldLoggerLevel, oldLoggerOut, oldLoggerFormatter := logger.GetLevel(), logger.Out, logger.Formatter
|
||||
t.Cleanup(func() {
|
||||
logger.SetLevel(oldLoggerLevel)
|
||||
logger.SetOutput(oldLoggerOut)
|
||||
logger.SetFormatter(oldLoggerFormatter)
|
||||
require.NoError(t, portFlag.Value.Set(oldPortValue))
|
||||
portFlag.Changed = oldPortChanged
|
||||
signalPort = oldSignalPort
|
||||
metricsPort = oldMetricsPort
|
||||
signalLetsencryptDomain = oldLetsencryptDomain
|
||||
signalLetsencryptEmail = oldLetsencryptEmail
|
||||
signalLetsencryptDataDir = oldLetsencryptDataDir
|
||||
signalCertFile = oldCertFile
|
||||
signalCertKey = oldCertKey
|
||||
logLevel = oldLogLevel
|
||||
logFile = oldLogFile
|
||||
signalPprofAddress = oldPprofAddress
|
||||
signalConfigPath = oldConfigPath
|
||||
})
|
||||
|
||||
signalConfigPath = ""
|
||||
require.NoError(t, portFlag.Value.Set(portValue))
|
||||
portFlag.Changed = portChanged
|
||||
return runCmd.PreRunE(runCmd, nil)
|
||||
}
|
||||
|
||||
func clearSignalConfigEnvironment(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for _, name := range []string{
|
||||
"NB_PORT",
|
||||
"NB_METRICS_PORT",
|
||||
"NB_LETSENCRYPT_DOMAIN",
|
||||
"NB_LETSENCRYPT_EMAIL",
|
||||
"NB_LETSENCRYPT_DATA_DIR",
|
||||
"NB_SSL_DIR",
|
||||
"NB_CERT_FILE",
|
||||
"NB_CERT_KEY",
|
||||
"NB_LOG_LEVEL",
|
||||
"NB_LOG_FILE",
|
||||
"NB_PPROF_ADDR",
|
||||
// Collapsed camelCase spellings that the shared loader derives automatically; they never
|
||||
// existed in the legacy NB_<FLAG_NAME> mapping, so the suite must not inherit them either.
|
||||
"NB_METRICSPORT",
|
||||
"NB_LETSENCRYPTDOMAIN",
|
||||
"NB_LETSENCRYPTEMAIL",
|
||||
"NB_LETSENCRYPTDATADIR",
|
||||
"NB_CERTFILE",
|
||||
"NB_CERTKEY",
|
||||
"NB_LOGLEVEL",
|
||||
"NB_LOGFILE",
|
||||
"NB_PPROFADDRESS",
|
||||
} {
|
||||
t.Setenv(name, "")
|
||||
require.NoError(t, os.Unsetenv(name))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigIgnoresPreviouslyUnboundMetricsPortEnvironment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "valid value", value: "9191"},
|
||||
{name: "invalid value", value: "abc"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
t.Setenv("NB_METRICS_PORT", test.value)
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
assert.NoError(t, err,
|
||||
"metrics-port was a non-persistent flag that the legacy NB_ mapping never visited, so NB_METRICS_PORT could not fail startup")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, defaultConfig().MetricsPort, cfg.MetricsPort,
|
||||
"metrics-port was a non-persistent flag that the legacy NB_ mapping never visited, so NB_METRICS_PORT must not move the metrics endpoint")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesLegacyEnvironmentAliasPrecedenceWithEmptyValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty preferred alias keeps legacy alias",
|
||||
env: map[string]string{"NB_LETSENCRYPT_DATA_DIR": "", "NB_SSL_DIR": "/legacy-certs"},
|
||||
expected: "/legacy-certs",
|
||||
},
|
||||
{
|
||||
name: "empty legacy alias clears preferred alias",
|
||||
env: map[string]string{"NB_LETSENCRYPT_DATA_DIR": "/preferred-certs", "NB_SSL_DIR": ""},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
for name, value := range test.env {
|
||||
t.Setenv(name, value)
|
||||
}
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, cfg.LetsencryptDataDir,
|
||||
"Legacy applied every present NB_ variable in flag order (letsencrypt-data-dir, then ssl-dir) into the same variable, so the last present alias won even when it was empty")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigIgnoresPreviouslyUnboundLogEnvironment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
field string
|
||||
}{
|
||||
{name: "valid log level", env: map[string]string{"NB_LOG_LEVEL": "debug"}},
|
||||
{name: "invalid log level", env: map[string]string{"NB_LOG_LEVEL": "verbose"}},
|
||||
{name: "empty log level", env: map[string]string{"NB_LOG_LEVEL": ""}},
|
||||
{name: "console log file", env: map[string]string{"NB_LOG_FILE": "console"}},
|
||||
{name: "syslog log file", env: map[string]string{"NB_LOG_FILE": "syslog"}},
|
||||
{name: "writable log file", env: map[string]string{"NB_LOG_FILE": filepath.Join(t.TempDir(), "signal.log")}},
|
||||
{name: "unwritable log file", env: map[string]string{"NB_LOG_FILE": filepath.Join(t.TempDir(), "missing", "dir", "signal.log")}},
|
||||
{name: "empty log file", env: map[string]string{"NB_LOG_FILE": ""}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
for name, value := range test.env {
|
||||
t.Setenv(name, value)
|
||||
}
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaultConfig().LogLevel, cfg.LogLevel,
|
||||
"log-level lived on the root command; the legacy NB_ mapping only visited run persistent flags, so NB_LOG_LEVEL was ignored and the level stayed info")
|
||||
assert.Equal(t, defaultConfig().LogFile, cfg.LogFile,
|
||||
"log-file lived on the root command; the legacy NB_ mapping only visited run persistent flags, so NB_LOG_FILE was ignored and logs went to the default file")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignalPreRunIgnoresPreviouslyUnboundLogLevelEnvironment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "valid log level", value: "debug"},
|
||||
{name: "invalid log level", value: "verbose"},
|
||||
{name: "empty log level", value: ""},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
t.Setenv("NB_LOG_LEVEL", test.value)
|
||||
t.Setenv("NB_LOG_FILE", "console")
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
err := executeSignalPreRun(t, "0", false)
|
||||
assert.NoError(t, err,
|
||||
"NB_LOG_LEVEL was never applied by the legacy NB_ mapping, so any value left the service starting at level info")
|
||||
assert.Equal(t, "info", logLevel,
|
||||
"NB_LOG_LEVEL was never applied by the legacy NB_ mapping, so the effective level must remain info")
|
||||
if err == nil {
|
||||
assert.Equal(t, log.InfoLevel, log.StandardLogger().GetLevel(),
|
||||
"Legacy initialized the logger with the flag-only level info regardless of NB_LOG_LEVEL")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigIgnoresCollapsedEnvironmentNames(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
t.Setenv("NB_METRICSPORT", "1234")
|
||||
t.Setenv("NB_LETSENCRYPTDOMAIN", "auto.example.com")
|
||||
t.Setenv("NB_LETSENCRYPTEMAIL", "auto@example.com")
|
||||
t.Setenv("NB_LETSENCRYPTDATADIR", "/auto-certs")
|
||||
t.Setenv("NB_CERTFILE", "/auto/tls.crt")
|
||||
t.Setenv("NB_CERTKEY", "/auto/tls.key")
|
||||
t.Setenv("NB_LOGLEVEL", "trace")
|
||||
t.Setenv("NB_LOGFILE", "console")
|
||||
t.Setenv("NB_PPROFADDRESS", "localhost:1")
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, defaultConfig(), cfg,
|
||||
"Legacy only mapped NB_<FLAG_NAME> with underscores between words; collapsed camelCase spellings such as NB_METRICSPORT did not exist and were ignored")
|
||||
}
|
||||
|
||||
func TestLoadConfigPreservesDocumentedEnvironmentNamesOverCollapsedNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
actual func(*Config) any
|
||||
expected any
|
||||
}{
|
||||
{
|
||||
name: "pprof address",
|
||||
env: map[string]string{"NB_PPROFADDRESS": "auto:2", "NB_PPROF_ADDR": "explicit:1"},
|
||||
actual: func(cfg *Config) any { return cfg.PprofAddress },
|
||||
expected: "explicit:1",
|
||||
},
|
||||
{
|
||||
name: "metrics port",
|
||||
env: map[string]string{"NB_METRICSPORT": "7777", "NB_METRICS_PORT": "8888"},
|
||||
actual: func(cfg *Config) any { return cfg.MetricsPort },
|
||||
expected: defaultConfig().MetricsPort,
|
||||
},
|
||||
{
|
||||
name: "letsencrypt data dir",
|
||||
env: map[string]string{"NB_LETSENCRYPTDATADIR": "/auto-certs", "NB_LETSENCRYPT_DATA_DIR": "/preferred-certs", "NB_SSL_DIR": "/legacy-certs"},
|
||||
actual: func(cfg *Config) any { return cfg.LetsencryptDataDir },
|
||||
expected: "/legacy-certs",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
for name, value := range test.env {
|
||||
t.Setenv(name, value)
|
||||
}
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, test.actual(cfg),
|
||||
"Legacy read only the documented NB_ names (NB_PPROF_ADDR directly, NB_<FLAG_NAME> for persistent flags); an undocumented collapsed spelling must not override them")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignalExplicitZeroPortFlagRemainsCompatible(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
}{
|
||||
{name: "no TLS"},
|
||||
{name: "letsencrypt", env: map[string]string{"NB_LETSENCRYPT_DOMAIN": "signal.example.com"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
for name, value := range test.env {
|
||||
t.Setenv(name, value)
|
||||
}
|
||||
t.Setenv("NB_LOG_FILE", "console")
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
require.NoError(t, executeSignalPreRun(t, "0", true))
|
||||
assert.Equal(t, 0, signalPort,
|
||||
"Legacy skipped the 80/443 default heuristic whenever the port flag was Changed, so `--port 0` kept 0 and bound an ephemeral port")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacySignalRunCommandHadNoConfigFlag(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
configFlag := runCmd.PersistentFlags().Lookup("config")
|
||||
assert.Nil(t, configFlag, "Legacy Signal had no --config flag; cobra rejected it as an unknown flag")
|
||||
|
||||
if configFlag != nil {
|
||||
oldValue, oldChanged := configFlag.Value.String(), configFlag.Changed
|
||||
oldConfigPath := signalConfigPath
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, configFlag.Value.Set(oldValue))
|
||||
configFlag.Changed = oldChanged
|
||||
signalConfigPath = oldConfigPath
|
||||
})
|
||||
}
|
||||
|
||||
err := runCmd.ParseFlags([]string{"--config", filepath.Join(t.TempDir(), "signal.yaml")})
|
||||
assert.Error(t, err, "Legacy Signal rejected `run --config <path>` with `unknown flag: --config` and exited with a usage error")
|
||||
}
|
||||
|
||||
func TestSignalPortFlagHelpRetainsLegacyDefault(t *testing.T) {
|
||||
portFlag := runCmd.PersistentFlags().Lookup("port")
|
||||
require.NotNil(t, portFlag, "Signal port flag should be registered")
|
||||
|
||||
assert.Equal(t, "80", portFlag.DefValue,
|
||||
"Legacy registered --port with default 80, so `run --help` rendered a `(default 80)` suffix")
|
||||
assert.Regexp(t, `--port int\s+Server port to listen on .*\(default 80\)`, runCmd.PersistentFlags().FlagUsages(),
|
||||
"Legacy `run --help` printed `(default 80)` after the --port description")
|
||||
}
|
||||
|
||||
func TestLegacySignalInformationalCommandsAppliedEnvironmentAtStartup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{name: "version", args: []string{"--version"}},
|
||||
{name: "run help", args: []string{"run", "--help"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=^TestSignalHelperProcess$")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"NB_SIGNAL_TEST_HELPER_PROCESS=1",
|
||||
"NB_SIGNAL_TEST_HELPER_ARGS="+strings.Join(test.args, " "),
|
||||
"NB_PORT=invalid",
|
||||
)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
require.NoError(t, cmd.Run(), "helper process should exit cleanly, stdout: %s stderr: %s", stdout.String(), stderr.String())
|
||||
|
||||
assert.Contains(t, stderr.String(), "unable to configure flag port using variable NB_PORT",
|
||||
"Legacy applied NB_ variables to flags in init() for every invocation, so an invalid NB_PORT logged a warning even for informational commands")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignalHelperProcess executes the Signal root command inside a child test process. It is only
|
||||
// active when spawned by TestLegacySignalInformationalCommandsAppliedEnvironmentAtStartup.
|
||||
func TestSignalHelperProcess(t *testing.T) {
|
||||
if os.Getenv("NB_SIGNAL_TEST_HELPER_PROCESS") != "1" {
|
||||
t.Skip("helper process for subprocess-based tests")
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(strings.Fields(os.Getenv("NB_SIGNAL_TEST_HELPER_ARGS")))
|
||||
require.NoError(t, rootCmd.Execute())
|
||||
}
|
||||
|
||||
func TestLoadConfigFileCannotEnablePprofAsLegacy(t *testing.T) {
|
||||
clearSignalConfigEnvironment(t)
|
||||
configPath := filepath.Join(t.TempDir(), "signal.yaml")
|
||||
require.NoError(t, os.WriteFile(configPath, []byte("pprofAddress: localhost:6060\n"), 0o600))
|
||||
require.NoError(t, runCmd.ParseFlags(nil))
|
||||
|
||||
cfg, err := loadConfig(runCmd, configPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", cfg.PprofAddress,
|
||||
"Legacy enabled pprof only from os.Getenv(\"NB_PPROF_ADDR\") at run time; no file source could turn it on")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user