feat(config): load and validate env-var config
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// Package config loads service configuration from environment variables.
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config holds all runtime configuration for the service.
|
||||
type Config struct {
|
||||
APIKey string
|
||||
StacksRoot string
|
||||
Port string
|
||||
LogLevel string
|
||||
UpdateTimeout time.Duration
|
||||
OptInLabel string
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables, applies defaults,
|
||||
// and validates required fields. Returns an error if validation fails so
|
||||
// the service can fail-fast at startup.
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{
|
||||
APIKey: os.Getenv("UPDATER_API_KEY"),
|
||||
StacksRoot: getenvDefault("STACKS_ROOT", "/home/shcizo/self-hosted"),
|
||||
Port: getenvDefault("PORT", "8080"),
|
||||
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
||||
OptInLabel: getenvDefault("OPT_IN_LABEL", "se.shcizo.auto-update"),
|
||||
}
|
||||
|
||||
if cfg.APIKey == "" {
|
||||
return nil, errors.New("UPDATER_API_KEY is required")
|
||||
}
|
||||
|
||||
timeoutStr := getenvDefault("UPDATE_TIMEOUT", "5m")
|
||||
d, err := time.ParseDuration(timeoutStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UPDATE_TIMEOUT %q is not a valid duration: %w", timeoutStr, err)
|
||||
}
|
||||
cfg.UpdateTimeout = d
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getenvDefault(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shcizo/package-updater/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoad_RequiresAPIKey(t *testing.T) {
|
||||
t.Setenv("UPDATER_API_KEY", "")
|
||||
_, err := config.Load()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "UPDATER_API_KEY")
|
||||
}
|
||||
|
||||
func TestLoad_AppliesDefaults(t *testing.T) {
|
||||
t.Setenv("UPDATER_API_KEY", "secret")
|
||||
t.Setenv("STACKS_ROOT", "")
|
||||
t.Setenv("PORT", "")
|
||||
t.Setenv("LOG_LEVEL", "")
|
||||
t.Setenv("UPDATE_TIMEOUT", "")
|
||||
t.Setenv("OPT_IN_LABEL", "")
|
||||
|
||||
cfg, err := config.Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "secret", cfg.APIKey)
|
||||
require.Equal(t, "/home/shcizo/self-hosted", cfg.StacksRoot)
|
||||
require.Equal(t, "8080", cfg.Port)
|
||||
require.Equal(t, "info", cfg.LogLevel)
|
||||
require.Equal(t, 5*time.Minute, cfg.UpdateTimeout)
|
||||
require.Equal(t, "se.shcizo.auto-update", cfg.OptInLabel)
|
||||
}
|
||||
|
||||
func TestLoad_OverridesViaEnv(t *testing.T) {
|
||||
t.Setenv("UPDATER_API_KEY", "secret")
|
||||
t.Setenv("STACKS_ROOT", "/srv/stacks")
|
||||
t.Setenv("PORT", "9090")
|
||||
t.Setenv("LOG_LEVEL", "debug")
|
||||
t.Setenv("UPDATE_TIMEOUT", "30s")
|
||||
t.Setenv("OPT_IN_LABEL", "io.example.update")
|
||||
|
||||
cfg, err := config.Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/srv/stacks", cfg.StacksRoot)
|
||||
require.Equal(t, "9090", cfg.Port)
|
||||
require.Equal(t, "debug", cfg.LogLevel)
|
||||
require.Equal(t, 30*time.Second, cfg.UpdateTimeout)
|
||||
require.Equal(t, "io.example.update", cfg.OptInLabel)
|
||||
}
|
||||
|
||||
func TestLoad_InvalidTimeoutErrors(t *testing.T) {
|
||||
t.Setenv("UPDATER_API_KEY", "secret")
|
||||
t.Setenv("UPDATE_TIMEOUT", "not-a-duration")
|
||||
_, err := config.Load()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "UPDATE_TIMEOUT")
|
||||
}
|
||||
Reference in New Issue
Block a user