Files
netbird/relay/cmd/config_test.go
T

656 lines
22 KiB
Go

package cmd
import (
"math"
"os"
"path/filepath"
"testing"
"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")
t.Cleanup(func() {
configPath = oldConfigPath
})
cfg, err := loadConfig(rootCmd)
require.NoError(t, err)
assert.Equal(t, "rels://relay.example.com:443", cfg.ExposedAddress, "Example config should load")
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"
exposedAddress: "relay.example.com:443"
authSecret: file-secret
metricsPort: 9091
enableSTUN: true
stunPorts: [3478, 3479]
`), 0o600))
t.Setenv("NB_METRICS_PORT", "9191")
require.NoError(t, rootCmd.ParseFlags(nil))
listenFlag := rootCmd.Flags().Lookup("listen-address")
oldListen := listenFlag.Value.String()
oldChanged := listenFlag.Changed
t.Cleanup(func() {
require.NoError(t, listenFlag.Value.Set(oldListen))
listenFlag.Changed = oldChanged
})
require.NoError(t, listenFlag.Value.Set(":7443"))
listenFlag.Changed = true
oldConfigPath := configPath
configPath = path
t.Cleanup(func() {
configPath = oldConfigPath
})
cfg, err := loadConfig(rootCmd)
require.NoError(t, err)
assert.Equal(t, ":7443", cfg.ListenAddress, "Flags should override the configuration file")
assert.Equal(t, 9191, cfg.MetricsPort, "Environment should override the configuration file")
assert.Equal(t, "relay.example.com:443", cfg.ExposedAddress, "File values should override defaults")
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 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 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")
})
}
}