This commit is contained in:
2024-11-18 16:28:45 +02:00
parent f0452ff9f7
commit 825c7e3391
34 changed files with 624 additions and 426 deletions

View File

@@ -8,48 +8,42 @@ import (
"github.com/rs/zerolog"
)
// Environment variable names
// Environment variable names.
const (
EnvLogLevel = "LOG_LEVEL"
EnvRootPath = "ROOT_PATH"
EnvNumWorkers = "NUM_OF_WORKERS"
EnvWorkerBatchSize = "WORKER_BATCH_SIZE"
EnvMaxResponseSize = "MAX_RESPONSE_SIZE"
EnvResponseTimeout = "RESPONSE_TIMEOUT"
EnvLogLevel = "LOG_LEVEL"
EnvNumWorkers = "NUM_OF_WORKERS"
EnvWorkerBatchSize = "WORKER_BATCH_SIZE"
EnvMaxResponseSize = "MAX_RESPONSE_SIZE"
EnvResponseTimeout = "RESPONSE_TIMEOUT"
EnvPanicOnUnexpectedError = "PANIC_ON_UNEXPECTED_ERROR"
EnvBlacklistPath = "BLACKLIST_PATH"
)
// Config holds the application configuration loaded from environment variables
// Config holds the application configuration loaded from environment variables.
type Config struct {
LogLevel zerolog.Level // Logging level (debug, info, warn, error)
RootPath string // Root path for the application
MaxResponseSize int // Maximum size of response in bytes
NumOfWorkers int // Number of concurrent workers
ResponseTimeout int // Timeout for responses in seconds
WorkerBatchSize int // Batch size for worker processing
LogLevel zerolog.Level // Logging level (debug, info, warn, error)
MaxResponseSize int // Maximum size of response in bytes
NumOfWorkers int // Number of concurrent workers
ResponseTimeout int // Timeout for responses in seconds
WorkerBatchSize int // Batch size for worker processing
PanicOnUnexpectedError bool // Panic on unexpected errors when visiting a URL
BlacklistPath string // File that has blacklisted strings of "host:port"
}
// String returns a string representation of the configuration
func (c *Config) String() string {
return fmt.Sprintf(
"Config{LogLevel: %s, RootPath: %s, MaxResponseSize: %d, NumWorkers: %d, ResponseTimeout: %d, WorkerBatchSize: %d}",
c.LogLevel, c.RootPath, c.MaxResponseSize, c.NumOfWorkers, c.ResponseTimeout, c.WorkerBatchSize,
)
}
var CONFIG Config //nolint:gochecknoglobals
var CONFIG Config
// parsePositiveInt parses and validates positive integer values
// parsePositiveInt parses and validates positive integer values.
func parsePositiveInt(param, value string) (int, error) {
val, err := strconv.Atoi(value)
if err != nil {
return 0, &ValidationError{
return 0, ValidationError{
Param: param,
Value: value,
Reason: "must be a valid integer",
}
}
if val <= 0 {
return 0, &ValidationError{
return 0, ValidationError{
Param: param,
Value: value,
Reason: "must be positive",
@@ -58,6 +52,18 @@ func parsePositiveInt(param, value string) (int, error) {
return val, nil
}
func parseBool(param, value string) (bool, error) {
val, err := strconv.ParseBool(value)
if err != nil {
return false, ValidationError{
Param: param,
Value: value,
Reason: "cannot be converted to boolean",
}
}
return val, nil
}
// GetConfig loads and validates configuration from environment variables
func GetConfig() *Config {
config := &Config{}
@@ -67,7 +73,7 @@ func GetConfig() *Config {
EnvLogLevel: func(v string) error {
level, err := zerolog.ParseLevel(v)
if err != nil {
return &ValidationError{
return ValidationError{
Param: EnvLogLevel,
Value: v,
Reason: "must be one of: debug, info, warn, error",
@@ -76,16 +82,6 @@ func GetConfig() *Config {
config.LogLevel = level
return nil
},
EnvRootPath: func(v string) error {
if _, err := os.Stat(v); err != nil {
return &ConfigError{
Param: EnvRootPath,
Err: err,
}
}
config.RootPath = v
return nil
},
EnvNumWorkers: func(v string) error {
val, err := parsePositiveInt(EnvNumWorkers, v)
if err != nil {
@@ -118,6 +114,18 @@ func GetConfig() *Config {
config.ResponseTimeout = val
return nil
},
EnvPanicOnUnexpectedError: func(v string) error {
val, err := parseBool(EnvPanicOnUnexpectedError, v)
if err != nil {
return err
}
config.PanicOnUnexpectedError = val
return nil
},
EnvBlacklistPath: func(v string) error {
config.BlacklistPath = v
return nil
},
}
// Process each environment variable

View File

@@ -1,146 +0,0 @@
package config
import (
"os"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)
func TestGetConfig(t *testing.T) {
// Set up test environment variables
envVars := map[string]string{
EnvLogLevel: "debug",
EnvRootPath: ".",
EnvNumWorkers: "5",
EnvWorkerBatchSize: "100",
EnvMaxResponseSize: "1048576",
EnvResponseTimeout: "30",
}
for k, v := range envVars {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
// Get configuration
config := GetConfig()
// Assert configuration values
assert.Equal(t, zerolog.DebugLevel, config.LogLevel)
assert.Equal(t, ".", config.RootPath)
assert.Equal(t, 5, config.NumOfWorkers)
assert.Equal(t, 100, config.WorkerBatchSize)
assert.Equal(t, 1048576, config.MaxResponseSize)
assert.Equal(t, 30, config.ResponseTimeout)
}
func TestParsePositiveInt(t *testing.T) {
tests := []struct {
name string
param string
input string
want int
wantErr bool
errType interface{}
errMessage string
}{
{
name: "valid positive",
param: "TEST_PARAM",
input: "42",
want: 42,
wantErr: false,
},
{
name: "zero",
param: "TEST_PARAM",
input: "0",
wantErr: true,
errType: &ValidationError{},
errMessage: "invalid value '0' for TEST_PARAM: must be positive",
},
{
name: "negative",
param: "TEST_PARAM",
input: "-1",
wantErr: true,
errType: &ValidationError{},
errMessage: "invalid value '-1' for TEST_PARAM: must be positive",
},
{
name: "invalid",
param: "TEST_PARAM",
input: "abc",
wantErr: true,
errType: &ValidationError{},
errMessage: "invalid value 'abc' for TEST_PARAM: must be a valid integer",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parsePositiveInt(tt.param, tt.input)
if tt.wantErr {
assert.Error(t, err)
assert.IsType(t, tt.errType, err)
if tt.errMessage != "" {
assert.Equal(t, tt.errMessage, err.Error())
}
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func TestConfigValidation(t *testing.T) {
tests := []struct {
name string
envVars map[string]string
wantErr bool
errMessage string
}{
{
name: "invalid log level",
envVars: map[string]string{
EnvLogLevel: "invalid",
},
wantErr: true,
errMessage: "invalid value 'invalid' for LOG_LEVEL: must be one of: debug, info, warn, error",
},
{
name: "invalid worker count",
envVars: map[string]string{
EnvLogLevel: "debug",
EnvRootPath: ".",
EnvNumWorkers: "-1",
},
wantErr: true,
errMessage: "invalid value '-1' for NUM_OF_WORKERS: must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear environment
os.Clearenv()
// Set required environment variables
for k, v := range tt.envVars {
os.Setenv(k, v)
}
// Defer cleanup
defer os.Clearenv()
if tt.wantErr {
assert.PanicsWithError(t, tt.errMessage, func() {
GetConfig()
})
}
})
}
}

View File

@@ -2,27 +2,13 @@ package config
import "fmt"
// ConfigError represents a configuration error
type ConfigError struct {
Param string
Err error
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("configuration error for %s: %v", e.Param, e.Err)
}
func (e *ConfigError) Unwrap() error {
return e.Err
}
// ValidationError represents a validation error
// ValidationError represents a config validation error
type ValidationError struct {
Param string
Value string
Reason string
}
func (e *ValidationError) Error() string {
func (e ValidationError) Error() string {
return fmt.Sprintf("invalid value '%s' for %s: %s", e.Value, e.Param, e.Reason)
}