Files
scratchbox/internal/config/config_test.go
T
2026-05-29 22:39:50 -05:00

265 lines
6.5 KiB
Go

package config
import (
"net/netip"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestValidateAppliesDefaultsAndParsedFields(t *testing.T) {
t.Parallel()
cfg := defaults()
cfg.Storage.DataDir = t.TempDir()
if err := cfg.Validate(); err != nil {
t.Fatalf("Validate() returned error: %v", err)
}
if cfg.Limits.MaxUploadSizeBytes != 100_000_000 {
t.Fatalf("MaxUploadSizeBytes = %d, want %d", cfg.Limits.MaxUploadSizeBytes, int64(100_000_000))
}
if cfg.Limits.DefaultTTLDuration != time.Hour {
t.Fatalf("DefaultTTLDuration = %s, want 1h", cfg.Limits.DefaultTTLDuration)
}
if cfg.Storage.CleanupIntervalParsed != time.Minute {
t.Fatalf("CleanupIntervalParsed = %s, want 1m", cfg.Storage.CleanupIntervalParsed)
}
if len(cfg.Security.AllowedPrefixes) != 0 {
t.Fatalf("AllowedPrefixes len = %d, want 0", len(cfg.Security.AllowedPrefixes))
}
}
func TestLoadParsesConfiguredFields(t *testing.T) {
t.Parallel()
tmp := t.TempDir()
dataDir := filepath.Join(tmp, "store")
configPath := filepath.Join(tmp, "config.yaml")
content := `
server:
listen_addr: "127.0.0.1:9090"
limits:
max_upload_size: "2MiB"
default_ttl: "2h"
storage:
data_dir: "` + dataDir + `"
cleanup_interval: "45s"
security:
allowed_ips:
- "127.0.0.1"
- "10.0.0.0/8"
trust_proxy_headers: true
rate_limit:
enabled: true
requests_per_minute: 12
burst: 4
`
if err := os.WriteFile(configPath, []byte(content), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
cfg, err := Load(configPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.Server.ListenAddr != "127.0.0.1:9090" {
t.Fatalf("ListenAddr = %q, want %q", cfg.Server.ListenAddr, "127.0.0.1:9090")
}
if cfg.Limits.MaxUploadSizeBytes != 2*1024*1024 {
t.Fatalf("MaxUploadSizeBytes = %d, want %d", cfg.Limits.MaxUploadSizeBytes, int64(2*1024*1024))
}
if cfg.Limits.DefaultTTLDuration != 2*time.Hour {
t.Fatalf("DefaultTTLDuration = %s, want 2h", cfg.Limits.DefaultTTLDuration)
}
if cfg.Storage.CleanupIntervalParsed != 45*time.Second {
t.Fatalf("CleanupIntervalParsed = %s, want 45s", cfg.Storage.CleanupIntervalParsed)
}
if !cfg.Security.TrustProxyHeaders {
t.Fatalf("TrustProxyHeaders = false, want true")
}
if cfg.Security.RateLimit.RequestsPerMinute != 12 || cfg.Security.RateLimit.Burst != 4 {
t.Fatalf("unexpected rate_limit values: %+v", cfg.Security.RateLimit)
}
if len(cfg.Security.AllowedPrefixes) != 2 {
t.Fatalf("AllowedPrefixes len = %d, want 2", len(cfg.Security.AllowedPrefixes))
}
}
func TestValidateRejectsInvalidValues(t *testing.T) {
t.Parallel()
tests := []struct {
name string
mutate func(*Config)
wantErr string
}{
{
name: "listen addr required",
mutate: func(cfg *Config) {
cfg.Server.ListenAddr = " "
},
wantErr: "server.listen_addr",
},
{
name: "invalid max upload size",
mutate: func(cfg *Config) {
cfg.Limits.MaxUploadSize = "oops"
},
wantErr: "limits.max_upload_size",
},
{
name: "invalid ttl parse",
mutate: func(cfg *Config) {
cfg.Limits.DefaultTTL = "not-a-duration"
},
wantErr: "limits.default_ttl",
},
{
name: "ttl above max",
mutate: func(cfg *Config) {
cfg.Limits.DefaultTTL = "25h"
},
wantErr: "limits.default_ttl",
},
{
name: "invalid allowlist entry",
mutate: func(cfg *Config) {
cfg.Security.AllowedIPs = []string{"not-an-ip"}
},
wantErr: "security.allowed_ips",
},
{
name: "invalid rate limit burst",
mutate: func(cfg *Config) {
cfg.Security.RateLimit.Burst = 0
},
wantErr: "security.rate_limit.burst",
},
{
name: "data dir required",
mutate: func(cfg *Config) {
cfg.Storage.DataDir = " "
},
wantErr: "storage.data_dir",
},
{
name: "cleanup parse failure",
mutate: func(cfg *Config) {
cfg.Storage.CleanupInterval = "not-a-duration"
},
wantErr: "storage.cleanup_interval",
},
{
name: "cleanup too small",
mutate: func(cfg *Config) {
cfg.Storage.CleanupInterval = "5s"
},
wantErr: "storage.cleanup_interval",
},
{
name: "invalid rate limit rpm",
mutate: func(cfg *Config) {
cfg.Security.RateLimit.RequestsPerMinute = 0
},
wantErr: "security.rate_limit.requests_per_minute",
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
cfg := defaults()
cfg.Storage.DataDir = t.TempDir()
tc.mutate(&cfg)
err := cfg.Validate()
if err == nil {
t.Fatalf("Validate() error = nil, want non-nil")
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("Validate() error = %q, want substring %q", err.Error(), tc.wantErr)
}
})
}
}
func TestLoadErrors(t *testing.T) {
t.Parallel()
_, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
if err == nil || !strings.Contains(err.Error(), "read config") {
t.Fatalf("Load() error = %v, want read config error", err)
}
tmp := t.TempDir()
configPath := filepath.Join(tmp, "bad.yaml")
if err := os.WriteFile(configPath, []byte("limits: [bad"), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
_, err = Load(configPath)
if err == nil || !strings.Contains(err.Error(), "parse config yaml") {
t.Fatalf("Load() error = %v, want yaml parse error", err)
}
}
func TestParseAllowedPrefixesTrimsAndParses(t *testing.T) {
t.Parallel()
prefixes, err := parseAllowedPrefixes([]string{" 192.0.2.4 ", " ", "2001:db8::/32"})
if err != nil {
t.Fatalf("parseAllowedPrefixes() error = %v", err)
}
if len(prefixes) != 2 {
t.Fatalf("prefixes len = %d, want 2", len(prefixes))
}
wantIP := netip.MustParsePrefix("192.0.2.4/32")
if prefixes[0] != wantIP {
t.Fatalf("prefixes[0] = %v, want %v", prefixes[0], wantIP)
}
}
func TestParseHumanSizeVariants(t *testing.T) {
t.Parallel()
tests := []struct {
in string
want int64
wantErr string
}{
{in: "10", want: 10},
{in: "1.5KB", want: 1500},
{in: "2MiB", want: 2 * 1024 * 1024},
{in: "0", wantErr: "size must be > 0"},
{in: "abc", wantErr: "does not start with a number"},
{in: "5XB", wantErr: "unsupported unit"},
{in: "", wantErr: "size is empty"},
}
for _, tc := range tests {
tc := tc
t.Run(tc.in, func(t *testing.T) {
t.Parallel()
got, err := parseHumanSize(tc.in)
if tc.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("parseHumanSize(%q) err=%v, want contains %q", tc.in, err, tc.wantErr)
}
return
}
if err != nil {
t.Fatalf("parseHumanSize(%q) error = %v", tc.in, err)
}
if got != tc.want {
t.Fatalf("parseHumanSize(%q) = %d, want %d", tc.in, got, tc.want)
}
})
}
}