package config_test import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "sneak.berlin/go/webhooker/internal/config" "sneak.berlin/go/webhooker/internal/globals" "sneak.berlin/go/webhooker/internal/logger" ) // testEnvKey is a throwaway variable name used only by the helper // tables below, so they cannot disturb real configuration. const testEnvKey = "WEBHOOKER_TEST_VALUE" // Real configuration variables exercised by the config.New tests. const ( envKeyPort = "PORT" envKeyDebug = "DEBUG" envKeyMaintenanceMode = "MAINTENANCE_MODE" ) // envBoolCase is one row of the envBool table. type envBoolCase struct { name string set bool value string defaultValue bool expectError bool expected bool } // envBoolCases is the envBool table, kept out of the test body so // the test itself stays readable. func envBoolCases() []envBoolCase { return []envBoolCase{ { name: "unset uses default false", defaultValue: false, expected: false, }, { name: "unset uses default true", defaultValue: true, expected: true, }, { name: "empty uses default true", set: true, value: "", defaultValue: true, expected: true, }, { name: "true is parsed", set: true, value: "true", expected: true, }, { name: "one is parsed", set: true, value: "1", expected: true, }, { name: "False is parsed", set: true, value: "False", defaultValue: true, expected: false, }, { name: "zero is parsed", set: true, value: "0", defaultValue: true, expected: false, }, { name: "yes is rejected", set: true, value: "yes", expectError: true, }, { name: "on is rejected", set: true, value: "on", expectError: true, }, { name: "typo is rejected", set: true, value: "ture", expectError: true, }, } } func TestEnvBool(t *testing.T) { for _, tt := range envBoolCases() { t.Run(tt.name, func(t *testing.T) { // Cannot use t.Parallel() here because t.Setenv // is incompatible with parallel subtests. if tt.set { t.Setenv(testEnvKey, tt.value) } else { require.NoError(t, os.Unsetenv(testEnvKey)) } got, err := config.EnvBoolForTest( testEnvKey, tt.defaultValue, ) if tt.expectError { require.Error(t, err) assert.Contains(t, err.Error(), testEnvKey) assert.Contains(t, err.Error(), tt.value) return } require.NoError(t, err) assert.Equal(t, tt.expected, got) }) } } func TestEnvPositiveInt(t *testing.T) { const defaultValue = 7 tests := []struct { name string set bool value string expectError bool errIs error expected int }{ { name: "unset returns the default integer", expected: defaultValue, }, { name: "empty returns the default integer", set: true, value: "", expected: defaultValue, }, { name: "positive value is parsed", set: true, value: "42", expected: 42, }, { name: "unparseable value is rejected", set: true, value: "not-a-number", expectError: true, }, { name: "zero is rejected", set: true, value: "0", expectError: true, errIs: config.ErrNonPositiveValue, }, { name: "negative is rejected", set: true, value: "-5", expectError: true, errIs: config.ErrNonPositiveValue, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Cannot use t.Parallel() here because t.Setenv // is incompatible with parallel subtests. if tt.set { t.Setenv(testEnvKey, tt.value) } else { require.NoError(t, os.Unsetenv(testEnvKey)) } got, err := config.EnvPositiveIntForTest( testEnvKey, defaultValue, ) 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) } return } require.NoError(t, err) assert.Equal(t, tt.expected, got) }) } } func TestEnvPort(t *testing.T) { const defaultValue = 8080 tests := []struct { name string set bool value string expectError bool errIs error expected int }{ { name: "unset returns the default port", expected: defaultValue, }, { name: "valid port is parsed", set: true, value: "9000", expected: 9000, }, { name: "highest port is accepted", set: true, value: "65535", expected: 65535, }, { name: "unparseable value is rejected", set: true, value: "not-a-port", expectError: true, }, { name: "zero is rejected", set: true, value: "0", expectError: true, errIs: config.ErrNonPositiveValue, }, { name: "above the port range is rejected", set: true, value: "65536", expectError: true, errIs: config.ErrInvalidPort, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Cannot use t.Parallel() here because t.Setenv // is incompatible with parallel subtests. if tt.set { t.Setenv(testEnvKey, tt.value) } else { require.NoError(t, os.Unsetenv(testEnvKey)) } got, err := config.EnvPortForTest( testEnvKey, defaultValue, ) if tt.expectError { require.Error(t, err) assert.Contains(t, err.Error(), testEnvKey) if tt.errIs != nil { require.ErrorIs(t, err, tt.errIs) } return } require.NoError(t, err) assert.Equal(t, tt.expected, got) }) } } // buildConfig constructs a Config through fx exactly as the // application does, returning the config and any construction error. func buildConfig(t *testing.T) (*config.Config, error) { t.Helper() var cfg *config.Config app := fx.New( fx.NopLogger, fx.Provide( globals.New, logger.New, config.New, ), fx.Populate(&cfg), ) return cfg, app.Err() } func TestNewRejectsBadEnvValues(t *testing.T) { tests := []struct { name string key string value string expectError bool check func(t *testing.T, cfg *config.Config) }{ { name: "valid PORT is used", key: envKeyPort, value: "9001", check: func(t *testing.T, cfg *config.Config) { t.Helper() assert.Equal(t, 9001, cfg.Port) }, }, { name: "unparseable PORT aborts startup", key: envKeyPort, value: "eighty-eighty", expectError: true, }, { name: "out-of-range PORT aborts startup", key: envKeyPort, value: "70000", expectError: true, }, { name: "valid DEBUG is used", key: envKeyDebug, value: "true", check: func(t *testing.T, cfg *config.Config) { t.Helper() assert.True(t, cfg.Debug) }, }, { name: "unparseable DEBUG aborts startup", key: envKeyDebug, value: "ture", expectError: true, }, { name: "unparseable MAINTENANCE_MODE aborts startup", key: envKeyMaintenanceMode, value: "sometimes", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Cannot use t.Parallel() here because t.Setenv // is incompatible with parallel subtests. t.Setenv("WEBHOOKER_ENVIRONMENT", "dev") t.Setenv(tt.key, tt.value) cfg, err := buildConfig(t) if tt.expectError { require.Error(t, err) assert.Contains(t, err.Error(), tt.key) assert.Contains(t, err.Error(), tt.value) return } require.NoError(t, err) require.NotNil(t, cfg) tt.check(t, cfg) }) } } // TestNewUsesDefaultsWhenUnset proves the fail-loud behaviour did not // break the legitimate unset case: absent variables still get their // documented defaults. func TestNewUsesDefaultsWhenUnset(t *testing.T) { t.Setenv("WEBHOOKER_ENVIRONMENT", "dev") for _, key := range []string{ envKeyPort, envKeyDebug, envKeyMaintenanceMode, } { require.NoError(t, os.Unsetenv(key)) } cfg, err := buildConfig(t) require.NoError(t, err) require.NotNil(t, cfg) assert.Equal(t, 8080, cfg.Port) assert.False(t, cfg.Debug) assert.False(t, cfg.MaintenanceMode) }