Isolate config tests from the shell; any out-of-range PORT is ErrInvalidPort (closes #94)
check / check (push) Successful in 3m17s

Config tests that unset a variable now restore it when they end, and
every test that builds a Config first unsets every variable the config
reads, so a value exported in the developer's shell cannot change the
result. TestEnvPort checks that the bad value appears in its errors.

A PORT of zero or below now wraps ErrInvalidPort, as one above 65535
already did.

The README configuration table and the Settings page now say that a
RETENTION_SWEEP_INTERVAL that does not parse, or is zero or negative,
fails startup.

Model: opus-5-5
This commit is contained in:
2026-10-02 14:16:36 +00:00
parent e8379272ae
commit 6a2a789809
7 changed files with 87 additions and 86 deletions
+45 -11
View File
@@ -38,6 +38,41 @@ const (
bindAddressSample = "10.1.2.3"
)
// configEnvKeys is every variable config.New reads. Its tests unset
// all of them before setting the ones under test, so a variable
// exported in the developer's shell cannot change their outcome.
func configEnvKeys() []string {
return []string{
"WEBHOOKER_ENVIRONMENT",
envKeyPort,
envKeyBindAddress,
"DATA_DIR",
envKeyDebug,
"METRICS_USERNAME",
"METRICS_PASSWORD",
envKeySentryDSN,
"RETENTION_SWEEP_INTERVAL",
"SESSION_IDLE_TIMEOUT",
"RECEIVER_RATE_LIMIT",
"TRUSTED_PROXIES",
"ALLOWED_EGRESS_CIDRS",
}
}
// unsetEnv makes each key absent for the rest of the test and puts
// back whatever it held when the test ends. t.Setenv registers that
// restore; the Unsetenv after it is what makes the key absent, since
// a key set to the empty string is still present, and godotenv will
// not overwrite a present key.
func unsetEnv(t *testing.T, keys ...string) {
t.Helper()
for _, key := range keys {
t.Setenv(key, "")
require.NoError(t, os.Unsetenv(key))
}
}
// envBoolCase is one row of the envBool table.
type envBoolCase struct {
name string
@@ -124,7 +159,7 @@ func TestEnvBool(t *testing.T) {
if tt.set {
t.Setenv(testEnvKey, tt.value)
} else {
require.NoError(t, os.Unsetenv(testEnvKey))
unsetEnv(t, testEnvKey)
}
got, err := config.EnvBoolForTest(
@@ -194,6 +229,7 @@ func TestEnvPositiveInt(t *testing.T) {
},
}
//nolint:dupl // TestEnvPort makes the same checks, on purpose.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Cannot use t.Parallel() here because t.Setenv
@@ -201,7 +237,7 @@ func TestEnvPositiveInt(t *testing.T) {
if tt.set {
t.Setenv(testEnvKey, tt.value)
} else {
require.NoError(t, os.Unsetenv(testEnvKey))
unsetEnv(t, testEnvKey)
}
got, err := config.EnvPositiveIntForTest(
@@ -264,7 +300,7 @@ func TestEnvPort(t *testing.T) {
set: true,
value: "0",
expectError: true,
errIs: config.ErrNonPositiveValue,
errIs: config.ErrInvalidPort,
},
{
name: "above the port range is rejected",
@@ -275,6 +311,7 @@ func TestEnvPort(t *testing.T) {
},
}
//nolint:dupl // TestEnvPositiveInt makes the same checks, on purpose.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Cannot use t.Parallel() here because t.Setenv
@@ -282,7 +319,7 @@ func TestEnvPort(t *testing.T) {
if tt.set {
t.Setenv(testEnvKey, tt.value)
} else {
require.NoError(t, os.Unsetenv(testEnvKey))
unsetEnv(t, testEnvKey)
}
got, err := config.EnvPortForTest(
@@ -292,6 +329,7 @@ func TestEnvPort(t *testing.T) {
if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), testEnvKey)
assert.Contains(t, err.Error(), tt.value)
if tt.errIs != nil {
require.ErrorIs(t, err, tt.errIs)
@@ -322,7 +360,7 @@ func TestEnvBindAddress(t *testing.T) {
if tt.set {
t.Setenv(testEnvKey, tt.value)
} else {
require.NoError(t, os.Unsetenv(testEnvKey))
unsetEnv(t, testEnvKey)
}
got, err := config.EnvBindAddressForTest(
@@ -485,6 +523,7 @@ func TestNewRejectsBadEnvValues(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
// Cannot use t.Parallel() here because t.Setenv
// is incompatible with parallel subtests.
unsetEnv(t, configEnvKeys()...)
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
t.Setenv(tt.key, tt.value)
@@ -646,14 +685,9 @@ func sentryEnvValueCases() []badEnvValueCase {
// break the legitimate unset case: absent variables still get their
// documented defaults.
func TestNewUsesDefaultsWhenUnset(t *testing.T) {
unsetEnv(t, configEnvKeys()...)
t.Setenv("WEBHOOKER_ENVIRONMENT", "dev")
for _, key := range []string{
envKeyPort, envKeyDebug, envKeyBindAddress, envKeySentryDSN,
} {
require.NoError(t, os.Unsetenv(key))
}
cfg, err := buildConfig(t)
require.NoError(t, err)
require.NotNil(t, cfg)