feat(config): env-based configuration with validation

This commit is contained in:
2026-07-01 16:27:31 +07:00
parent 353f1e9dd3
commit 0d42eb2db0
3 changed files with 100 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
package config
import (
"encoding/base64"
"fmt"
"os"
"strconv"
)
type Config struct {
HTTPAddr string
DatabaseURL string
AuthUser string
AuthPass string
EncKey []byte
SessionSecret []byte
WorkerConcurrency int
}
func Load() (Config, error) {
c := Config{
HTTPAddr: getenv("HTTP_ADDR", ":8080"),
DatabaseURL: os.Getenv("DATABASE_URL"),
AuthUser: os.Getenv("AUTH_USER"),
AuthPass: os.Getenv("AUTH_PASS"),
SessionSecret: []byte(os.Getenv("SESSION_SECRET")),
WorkerConcurrency: 4,
}
if v := os.Getenv("WORKER_CONCURRENCY"); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n < 1 {
return Config{}, fmt.Errorf("WORKER_CONCURRENCY invalid: %q", v)
}
c.WorkerConcurrency = n
}
for k, v := range map[string]string{
"DATABASE_URL": c.DatabaseURL, "AUTH_USER": c.AuthUser,
"AUTH_PASS": c.AuthPass, "SESSION_SECRET": string(c.SessionSecret),
} {
if v == "" {
return Config{}, fmt.Errorf("%s is required", k)
}
}
key, err := base64.StdEncoding.DecodeString(os.Getenv("ENC_KEY"))
if err != nil {
return Config{}, fmt.Errorf("ENC_KEY must be base64: %w", err)
}
if len(key) != 32 {
return Config{}, fmt.Errorf("ENC_KEY must decode to 32 bytes, got %d", len(key))
}
c.EncKey = key
return c, nil
}
func getenv(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
+37
View File
@@ -0,0 +1,37 @@
package config
import (
"encoding/base64"
"testing"
)
func TestLoadRequiresEncKey32Bytes(t *testing.T) {
t.Setenv("DATABASE_URL", "postgres://x")
t.Setenv("AUTH_USER", "admin")
t.Setenv("AUTH_PASS", "pass")
t.Setenv("SESSION_SECRET", "secret")
t.Setenv("ENC_KEY", base64.StdEncoding.EncodeToString(make([]byte, 16))) // wrong size
if _, err := Load(); err == nil {
t.Fatal("expected error for 16-byte ENC_KEY, got nil")
}
}
func TestLoadDefaults(t *testing.T) {
t.Setenv("DATABASE_URL", "postgres://x")
t.Setenv("AUTH_USER", "admin")
t.Setenv("AUTH_PASS", "pass")
t.Setenv("SESSION_SECRET", "secret")
t.Setenv("ENC_KEY", base64.StdEncoding.EncodeToString(make([]byte, 32)))
cfg, err := Load()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.HTTPAddr != ":8080" {
t.Errorf("HTTPAddr = %q, want :8080", cfg.HTTPAddr)
}
if cfg.WorkerConcurrency != 4 {
t.Errorf("WorkerConcurrency = %d, want 4", cfg.WorkerConcurrency)
}
}