This commit is contained in:
2026-01-02 04:02:02 +00:00
commit 3b8e171fdb
117 changed files with 21570 additions and 0 deletions
+395
View File
@@ -0,0 +1,395 @@
package config
import (
"fmt"
"time"
)
// Config is the main configuration struct
type Config struct {
Server ServerConfig `mapstructure:"server" json:"server"`
Storage StorageConfig `mapstructure:"storage" json:"storage"`
Metadata MetadataConfig `mapstructure:"metadata" json:"metadata"`
Cache CacheConfig `mapstructure:"cache" json:"cache"`
Security SecurityConfig `mapstructure:"security" json:"security"`
Auth AuthConfig `mapstructure:"auth" json:"auth"`
Network NetworkConfig `mapstructure:"network" json:"network"`
Logging LoggingConfig `mapstructure:"logging" json:"logging"`
Handlers HandlersConfig `mapstructure:"handlers" json:"handlers"`
}
// ServerConfig contains HTTP server configuration
type ServerConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
ReadTimeout time.Duration `mapstructure:"read_timeout" json:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout" json:"write_timeout"`
IdleTimeout time.Duration `mapstructure:"idle_timeout" json:"idle_timeout"`
TLS TLSConfig `mapstructure:"tls" json:"tls"`
}
// TLSConfig contains TLS/HTTPS configuration
type TLSConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
CertFile string `mapstructure:"cert_file" json:"cert_file"`
KeyFile string `mapstructure:"key_file" json:"key_file"`
}
// StorageConfig contains storage backend configuration
type StorageConfig struct {
Backend string `mapstructure:"backend" json:"backend"` // filesystem, s3, smb, nfs
Path string `mapstructure:"path" json:"path"`
Filesystem FilesystemConfig `mapstructure:"filesystem" json:"filesystem"`
S3 S3Config `mapstructure:"s3" json:"s3"`
SMB SMBConfig `mapstructure:"smb" json:"smb"`
Options map[string]interface{} `mapstructure:"options" json:"options"`
}
// FilesystemConfig contains local filesystem storage configuration
type FilesystemConfig struct {
BasePath string `mapstructure:"base_path" json:"base_path"`
}
// S3Config contains S3-compatible storage configuration
type S3Config struct {
Endpoint string `mapstructure:"endpoint" json:"endpoint"`
Region string `mapstructure:"region" json:"region"`
Bucket string `mapstructure:"bucket" json:"bucket"`
AccessKeyID string `mapstructure:"access_key_id" json:"access_key_id"`
SecretAccessKey string `mapstructure:"secret_access_key" json:"-"` // Don't serialize secrets
UseSSL bool `mapstructure:"use_ssl" json:"use_ssl"`
}
// SMBConfig contains SMB/CIFS storage configuration
type SMBConfig struct {
Host string `mapstructure:"host" json:"host"`
Share string `mapstructure:"share" json:"share"`
Username string `mapstructure:"username" json:"username"`
Password string `mapstructure:"password" json:"-"` // Don't serialize secrets
Domain string `mapstructure:"domain" json:"domain"`
}
// MetadataConfig contains metadata store configuration
type MetadataConfig struct {
Backend string `mapstructure:"backend" json:"backend"` // sqlite, postgresql, file
Connection string `mapstructure:"connection" json:"connection"`
SQLite SQLiteConfig `mapstructure:"sqlite" json:"sqlite"`
PostgreSQL PostgreSQLConfig `mapstructure:"postgresql" json:"postgresql"`
}
// SQLiteConfig contains SQLite-specific configuration
type SQLiteConfig struct {
Path string `mapstructure:"path" json:"path"`
WALMode bool `mapstructure:"wal_mode" json:"wal_mode"`
}
// PostgreSQLConfig contains PostgreSQL-specific configuration
type PostgreSQLConfig struct {
Host string `mapstructure:"host" json:"host"`
Port int `mapstructure:"port" json:"port"`
Database string `mapstructure:"database" json:"database"`
User string `mapstructure:"user" json:"user"`
Password string `mapstructure:"password" json:"-"` // Don't serialize secrets
SSLMode string `mapstructure:"ssl_mode" json:"ssl_mode"`
}
// CacheConfig contains cache management configuration
type CacheConfig struct {
DefaultTTL time.Duration `mapstructure:"default_ttl" json:"default_ttl"`
CleanupInterval time.Duration `mapstructure:"cleanup_interval" json:"cleanup_interval"`
MaxSizeBytes int64 `mapstructure:"max_size_bytes" json:"max_size_bytes"`
PerProjectQuota int64 `mapstructure:"per_project_quota" json:"per_project_quota"`
TTLOverrides map[string]time.Duration `mapstructure:"ttl_overrides" json:"ttl_overrides"` // Per ecosystem
}
// SecurityConfig contains security scanning configuration
type SecurityConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
ScanOnDownload bool `mapstructure:"scan_on_download" json:"scan_on_download"` // Scan packages on first download
RescanInterval time.Duration `mapstructure:"rescan_interval" json:"rescan_interval"` // How often to re-scan (e.g., 24h, 168h for weekly)
BlockOnSeverity string `mapstructure:"block_on_severity" json:"block_on_severity"` // none, low, medium, high, critical
BlockThresholds VulnerabilityThresholds `mapstructure:"block_thresholds" json:"block_thresholds"` // Max vulns per severity before blocking
UpdateDBOnStartup bool `mapstructure:"update_db_on_startup" json:"update_db_on_startup"` // Update vulnerability databases on startup
AllowedPackages []string `mapstructure:"allowed_packages" json:"allowed_packages"` // Packages that bypass security checks (format: "registry/name@version" or "registry/name")
IgnoredCVEs []string `mapstructure:"ignored_cves" json:"ignored_cves"` // CVE IDs to ignore globally (e.g., "CVE-2021-23337")
Scanners ScannersConfig `mapstructure:"scanners" json:"scanners"`
}
// VulnerabilityThresholds defines max allowed vulnerabilities per severity
type VulnerabilityThresholds struct {
Critical int `mapstructure:"critical" json:"critical"` // Max critical vulns (0 = block any)
High int `mapstructure:"high" json:"high"` // Max high vulns
Medium int `mapstructure:"medium" json:"medium"` // Max medium vulns
Low int `mapstructure:"low" json:"low"` // Max low vulns (-1 = unlimited)
}
// ScannersConfig contains individual scanner configurations
type ScannersConfig struct {
Trivy TrivyConfig `mapstructure:"trivy" json:"trivy"`
OSV OSVConfig `mapstructure:"osv" json:"osv"`
Static StaticConfig `mapstructure:"static" json:"static"`
}
// TrivyConfig contains Trivy scanner configuration
type TrivyConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
Timeout time.Duration `mapstructure:"timeout" json:"timeout"`
CacheDB string `mapstructure:"cache_db" json:"cache_db"`
}
// OSVConfig contains OSV scanner configuration
type OSVConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
APIURL string `mapstructure:"api_url" json:"api_url"`
Timeout time.Duration `mapstructure:"timeout" json:"timeout"`
}
// StaticConfig contains static analysis configuration
type StaticConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
MaxPackageSize int64 `mapstructure:"max_package_size" json:"max_package_size"`
CheckChecksums bool `mapstructure:"check_checksums" json:"check_checksums"`
BlockSuspicious bool `mapstructure:"block_suspicious" json:"block_suspicious"`
AllowedLicenses []string `mapstructure:"allowed_licenses" json:"allowed_licenses"`
}
// AuthConfig contains authentication configuration
type AuthConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
KeyExpiration time.Duration `mapstructure:"key_expiration" json:"key_expiration"`
BcryptCost int `mapstructure:"bcrypt_cost" json:"bcrypt_cost"`
AuditLog bool `mapstructure:"audit_log" json:"audit_log"`
}
// NetworkConfig contains network resilience configuration
type NetworkConfig struct {
ConnectTimeout time.Duration `mapstructure:"connect_timeout" json:"connect_timeout"`
ReadTimeout time.Duration `mapstructure:"read_timeout" json:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout" json:"write_timeout"`
MaxIdleConns int `mapstructure:"max_idle_conns" json:"max_idle_conns"`
MaxConnsPerHost int `mapstructure:"max_conns_per_host" json:"max_conns_per_host"`
RateLimit RateLimitConfig `mapstructure:"rate_limit" json:"rate_limit"`
CircuitBreaker CircuitBreakerConfig `mapstructure:"circuit_breaker" json:"circuit_breaker"`
Retry RetryConfig `mapstructure:"retry" json:"retry"`
}
// RateLimitConfig contains rate limiting configuration
type RateLimitConfig struct {
PerAPIKey int `mapstructure:"per_api_key" json:"per_api_key"`
PerIP int `mapstructure:"per_ip" json:"per_ip"`
BurstSize int `mapstructure:"burst_size" json:"burst_size"`
}
// CircuitBreakerConfig contains circuit breaker configuration
type CircuitBreakerConfig struct {
Threshold int `mapstructure:"threshold" json:"threshold"`
Timeout time.Duration `mapstructure:"timeout" json:"timeout"`
ResetInterval time.Duration `mapstructure:"reset_interval" json:"reset_interval"`
}
// RetryConfig contains retry policy configuration
type RetryConfig struct {
MaxAttempts int `mapstructure:"max_attempts" json:"max_attempts"`
InitialBackoff time.Duration `mapstructure:"initial_backoff" json:"initial_backoff"`
MaxBackoff time.Duration `mapstructure:"max_backoff" json:"max_backoff"`
}
// LoggingConfig contains logging configuration
type LoggingConfig struct {
Level string `mapstructure:"level" json:"level"` // debug, info, warn, error
Format string `mapstructure:"format" json:"format"` // json, pretty
}
// HandlersConfig contains package manager handler configurations
type HandlersConfig struct {
Go GoHandlerConfig `mapstructure:"go" json:"go"`
NPM NPMHandlerConfig `mapstructure:"npm" json:"npm"`
PyPI PyPIHandlerConfig `mapstructure:"pypi" json:"pypi"`
}
// GoHandlerConfig contains Go proxy configuration
type GoHandlerConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
UpstreamProxy string `mapstructure:"upstream_proxy" json:"upstream_proxy"`
ChecksumDB string `mapstructure:"checksum_db" json:"checksum_db"`
VerifyChecksums bool `mapstructure:"verify_checksums" json:"verify_checksums"`
}
// NPMHandlerConfig contains NPM registry configuration
type NPMHandlerConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
UpstreamRegistry string `mapstructure:"upstream_registry" json:"upstream_registry"`
}
// PyPIHandlerConfig contains PyPI configuration
type PyPIHandlerConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled"`
UpstreamURL string `mapstructure:"upstream_url" json:"upstream_url"`
SimpleAPIURL string `mapstructure:"simple_api_url" json:"simple_api_url"`
}
// Default returns a configuration with sensible defaults
func Default() *Config {
return &Config{
Server: ServerConfig{
Host: "0.0.0.0",
Port: 8080,
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
IdleTimeout: 2 * time.Minute,
TLS: TLSConfig{
Enabled: false,
},
},
Storage: StorageConfig{
Backend: "filesystem",
Path: "/var/cache/gohoarder",
Filesystem: FilesystemConfig{
BasePath: "/var/cache/gohoarder",
},
},
Metadata: MetadataConfig{
Backend: "sqlite",
Connection: "file:gohoarder.db?cache=shared&mode=rwc",
SQLite: SQLiteConfig{
Path: "gohoarder.db",
WALMode: true,
},
},
Cache: CacheConfig{
DefaultTTL: 7 * 24 * time.Hour,
CleanupInterval: 1 * time.Hour,
MaxSizeBytes: 500 * 1024 * 1024 * 1024, // 500GB
PerProjectQuota: 50 * 1024 * 1024 * 1024, // 50GB
TTLOverrides: map[string]time.Duration{
"npm": 7 * 24 * time.Hour,
"pip": 7 * 24 * time.Hour,
"go": 7 * 24 * time.Hour,
},
},
Security: SecurityConfig{
Enabled: false,
BlockOnSeverity: "high",
Scanners: ScannersConfig{
Trivy: TrivyConfig{
Enabled: false,
Timeout: 5 * time.Minute,
CacheDB: "/var/lib/trivy",
},
OSV: OSVConfig{
Enabled: false,
APIURL: "https://api.osv.dev",
Timeout: 30 * time.Second,
},
Static: StaticConfig{
Enabled: true,
MaxPackageSize: 2 * 1024 * 1024 * 1024, // 2GB
CheckChecksums: true,
BlockSuspicious: false,
},
},
},
Auth: AuthConfig{
Enabled: true,
KeyExpiration: 0, // Never expire
BcryptCost: 10,
AuditLog: true,
},
Network: NetworkConfig{
ConnectTimeout: 10 * time.Second,
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
MaxIdleConns: 100,
MaxConnsPerHost: 10,
RateLimit: RateLimitConfig{
PerAPIKey: 1000,
PerIP: 100,
BurstSize: 50,
},
CircuitBreaker: CircuitBreakerConfig{
Threshold: 5,
Timeout: 30 * time.Second,
ResetInterval: 60 * time.Second,
},
Retry: RetryConfig{
MaxAttempts: 3,
InitialBackoff: 1 * time.Second,
MaxBackoff: 30 * time.Second,
},
},
Logging: LoggingConfig{
Level: "info",
Format: "json",
},
Handlers: HandlersConfig{
Go: GoHandlerConfig{
Enabled: true,
UpstreamProxy: "https://proxy.golang.org",
ChecksumDB: "https://sum.golang.org",
VerifyChecksums: true,
},
NPM: NPMHandlerConfig{
Enabled: true,
UpstreamRegistry: "https://registry.npmjs.org",
},
PyPI: PyPIHandlerConfig{
Enabled: true,
UpstreamURL: "https://pypi.org",
SimpleAPIURL: "https://pypi.org/simple",
},
},
}
}
// Validate validates the configuration
func (c *Config) Validate() error {
// Validate server
if c.Server.Port < 1 || c.Server.Port > 65535 {
return fmt.Errorf("server.port must be between 1 and 65535, got %d", c.Server.Port)
}
// Validate storage backend
validStorageBackends := map[string]bool{"filesystem": true, "s3": true, "smb": true, "nfs": true}
if !validStorageBackends[c.Storage.Backend] {
return fmt.Errorf("storage.backend must be one of: filesystem, s3, smb, nfs; got %s", c.Storage.Backend)
}
// Validate metadata backend
validMetadataBackends := map[string]bool{"sqlite": true, "postgresql": true, "file": true}
if !validMetadataBackends[c.Metadata.Backend] {
return fmt.Errorf("metadata.backend must be one of: sqlite, postgresql, file; got %s", c.Metadata.Backend)
}
// Validate cache
if c.Cache.DefaultTTL < 0 {
return fmt.Errorf("cache.default_ttl cannot be negative")
}
if c.Cache.MaxSizeBytes < 0 {
return fmt.Errorf("cache.max_size_bytes cannot be negative")
}
// Validate security
validSeverities := map[string]bool{"none": true, "low": true, "medium": true, "high": true, "critical": true}
if !validSeverities[c.Security.BlockOnSeverity] {
return fmt.Errorf("security.block_on_severity must be one of: none, low, medium, high, critical; got %s", c.Security.BlockOnSeverity)
}
// Validate logging level
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
if !validLevels[c.Logging.Level] {
return fmt.Errorf("logging.level must be one of: debug, info, warn, error; got %s", c.Logging.Level)
}
// Validate logging format
validFormats := map[string]bool{"json": true, "pretty": true}
if !validFormats[c.Logging.Format] {
return fmt.Errorf("logging.format must be one of: json, pretty; got %s", c.Logging.Format)
}
// Validate auth
if c.Auth.BcryptCost < 4 || c.Auth.BcryptCost > 31 {
return fmt.Errorf("auth.bcrypt_cost must be between 4 and 31, got %d", c.Auth.BcryptCost)
}
return nil
}
+383
View File
@@ -0,0 +1,383 @@
package config
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type ConfigTestSuite struct {
suite.Suite
tempDir string
}
func TestConfigTestSuite(t *testing.T) {
suite.Run(t, new(ConfigTestSuite))
}
func (s *ConfigTestSuite) SetupTest() {
var err error
s.tempDir, err = os.MkdirTemp("", "gohoarder-config-test-*")
s.Require().NoError(err)
}
func (s *ConfigTestSuite) TearDownTest() {
os.RemoveAll(s.tempDir)
}
func (s *ConfigTestSuite) TestDefault() {
cfg := Default()
s.NotNil(cfg)
s.Equal("0.0.0.0", cfg.Server.Host)
s.Equal(8080, cfg.Server.Port)
s.Equal("filesystem", cfg.Storage.Backend)
s.Equal("sqlite", cfg.Metadata.Backend)
s.NoError(cfg.Validate())
}
func (s *ConfigTestSuite) TestValidate() {
tests := []struct {
name string
modify func(*Config)
expectError bool
errorSubstr string
}{
{
name: "valid_config",
modify: func(c *Config) {},
expectError: false,
},
{
name: "invalid_port_too_low",
modify: func(c *Config) {
c.Server.Port = 0
},
expectError: true,
errorSubstr: "port must be between",
},
{
name: "invalid_port_too_high",
modify: func(c *Config) {
c.Server.Port = 70000
},
expectError: true,
errorSubstr: "port must be between",
},
{
name: "invalid_storage_backend",
modify: func(c *Config) {
c.Storage.Backend = "invalid"
},
expectError: true,
errorSubstr: "storage.backend must be one of",
},
{
name: "invalid_metadata_backend",
modify: func(c *Config) {
c.Metadata.Backend = "mongodb"
},
expectError: true,
errorSubstr: "metadata.backend must be one of",
},
{
name: "negative_ttl",
modify: func(c *Config) {
c.Cache.DefaultTTL = -1 * time.Hour
},
expectError: true,
errorSubstr: "cannot be negative",
},
{
name: "negative_cache_size",
modify: func(c *Config) {
c.Cache.MaxSizeBytes = -100
},
expectError: true,
errorSubstr: "cannot be negative",
},
{
name: "invalid_severity",
modify: func(c *Config) {
c.Security.BlockOnSeverity = "super-high"
},
expectError: true,
errorSubstr: "block_on_severity must be one of",
},
{
name: "invalid_log_level",
modify: func(c *Config) {
c.Logging.Level = "verbose"
},
expectError: true,
errorSubstr: "logging.level must be one of",
},
{
name: "invalid_log_format",
modify: func(c *Config) {
c.Logging.Format = "xml"
},
expectError: true,
errorSubstr: "logging.format must be one of",
},
{
name: "invalid_bcrypt_cost_too_low",
modify: func(c *Config) {
c.Auth.BcryptCost = 3
},
expectError: true,
errorSubstr: "bcrypt_cost must be between",
},
{
name: "invalid_bcrypt_cost_too_high",
modify: func(c *Config) {
c.Auth.BcryptCost = 32
},
expectError: true,
errorSubstr: "bcrypt_cost must be between",
},
{
name: "valid_s3_backend",
modify: func(c *Config) {
c.Storage.Backend = "s3"
},
expectError: false,
},
{
name: "valid_postgresql_backend",
modify: func(c *Config) {
c.Metadata.Backend = "postgresql"
},
expectError: false,
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
cfg := Default()
tt.modify(cfg)
err := cfg.Validate()
if tt.expectError {
s.Error(err)
if tt.errorSubstr != "" {
s.Contains(err.Error(), tt.errorSubstr)
}
} else {
s.NoError(err)
}
})
}
}
func (s *ConfigTestSuite) TestLoad() {
tests := []struct {
name string
configYAML string
envVars map[string]string
expectError bool
validate func(*Config)
}{
{
name: "valid_yaml_config",
configYAML: `
server:
host: 127.0.0.1
port: 9000
storage:
backend: filesystem
path: /custom/path
logging:
level: debug
format: pretty
`,
expectError: false,
validate: func(cfg *Config) {
s.Equal("127.0.0.1", cfg.Server.Host)
s.Equal(9000, cfg.Server.Port)
s.Equal("/custom/path", cfg.Storage.Path)
s.Equal("debug", cfg.Logging.Level)
s.Equal("pretty", cfg.Logging.Format)
},
},
{
name: "env_var_override",
configYAML: `
server:
port: 8080
`,
envVars: map[string]string{
"GOHOARDER_SERVER_PORT": "9090",
},
expectError: false,
validate: func(cfg *Config) {
s.Equal(9090, cfg.Server.Port)
},
},
{
name: "invalid_yaml",
configYAML: `
server: [invalid
`,
expectError: true,
},
{
name: "validation_failure",
configYAML: `
server:
port: 100000
`,
expectError: true,
},
{
name: "complete_config",
configYAML: `
server:
host: 0.0.0.0
port: 8080
read_timeout: 300s
write_timeout: 300s
storage:
backend: s3
s3:
endpoint: s3.amazonaws.com
region: us-east-1
bucket: my-cache
access_key_id: AKIAIOSFODNN7EXAMPLE
secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
metadata:
backend: postgresql
postgresql:
host: localhost
port: 5432
database: gohoarder
user: postgres
password: secret
ssl_mode: require
cache:
default_ttl: 168h
max_size_bytes: 536870912000
security:
enabled: true
block_on_severity: high
scanners:
trivy:
enabled: true
timeout: 300s
auth:
enabled: true
bcrypt_cost: 12
`,
expectError: false,
validate: func(cfg *Config) {
s.Equal("s3", cfg.Storage.Backend)
s.Equal("s3.amazonaws.com", cfg.Storage.S3.Endpoint)
s.Equal("postgresql", cfg.Metadata.Backend)
s.Equal("localhost", cfg.Metadata.PostgreSQL.Host)
s.True(cfg.Security.Enabled)
s.Equal(12, cfg.Auth.BcryptCost)
},
},
}
for _, tt := range tests {
s.Run(tt.name, func() {
// Write config file
configPath := filepath.Join(s.tempDir, "config.yaml")
err := os.WriteFile(configPath, []byte(tt.configYAML), 0644)
s.Require().NoError(err)
// Set environment variables
for k, v := range tt.envVars {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
// Load config
cfg, err := Load(configPath)
if tt.expectError {
s.Error(err)
} else {
s.NoError(err)
s.NotNil(cfg)
if tt.validate != nil {
tt.validate(cfg)
}
}
})
}
}
func (s *ConfigTestSuite) TestLoadMissingFile() {
// Should return error when file explicitly specified but not found
cfg, err := Load("/nonexistent/path/to/config.yaml")
s.Error(err)
s.Nil(cfg)
}
func (s *ConfigTestSuite) TestLoadWithDefaults() {
// Invalid config path should return defaults
cfg := LoadWithDefaults("/invalid/path/config.yaml")
s.NotNil(cfg)
s.Equal(8080, cfg.Server.Port)
}
// Benchmark tests
func BenchmarkDefault(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Default()
}
}
func BenchmarkValidate(b *testing.B) {
cfg := Default()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = cfg.Validate()
}
}
// Table-driven edge cases
func TestConfigEdgeCases(t *testing.T) {
tests := []struct {
name string
config *Config
valid bool
}{
{
name: "minimal_config",
config: &Config{Server: ServerConfig{Port: 8080}, Storage: StorageConfig{Backend: "filesystem"}, Metadata: MetadataConfig{Backend: "sqlite"}, Logging: LoggingConfig{Level: "info", Format: "json"}, Security: SecurityConfig{BlockOnSeverity: "high"}, Auth: AuthConfig{BcryptCost: 10}},
valid: true,
},
{
name: "zero_ttl",
config: func() *Config { c := Default(); c.Cache.DefaultTTL = 0; return c }(),
valid: true, // Zero is valid (no caching)
},
{
name: "max_bcrypt_cost",
config: func() *Config { c := Default(); c.Auth.BcryptCost = 31; return c }(),
valid: true,
},
{
name: "min_bcrypt_cost",
config: func() *Config { c := Default(); c.Auth.BcryptCost = 4; return c }(),
valid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if tt.valid {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
})
}
}
+62
View File
@@ -0,0 +1,62 @@
package config
import (
"fmt"
"strings"
"github.com/spf13/viper"
)
// Load loads configuration from file and environment variables
func Load(configPath string) (*Config, error) {
v := viper.New()
// Set config file if provided
if configPath != "" {
v.SetConfigFile(configPath)
} else {
// Look for config.yaml in current directory and /etc/gohoarder
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath(".")
v.AddConfigPath("/etc/gohoarder")
v.AddConfigPath("$HOME/.gohoarder")
}
// Set environment variable prefix
v.SetEnvPrefix("GOHOARDER")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// Read config file
if err := v.ReadInConfig(); err != nil {
// If no config file found, use defaults
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
}
// Start with defaults
cfg := Default()
// Unmarshal into config struct
if err := v.Unmarshal(cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Validate configuration
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
}
return cfg, nil
}
// LoadWithDefaults loads configuration or returns defaults on error
func LoadWithDefaults(configPath string) *Config {
cfg, err := Load(configPath)
if err != nil {
return Default()
}
return cfg
}