Lots more changes

This commit is contained in:
2026-06-01 01:29:47 -05:00
parent 8bec7f0dda
commit 050ec138f3
20 changed files with 1773 additions and 689 deletions
+165 -11
View File
@@ -1,6 +1,7 @@
package config
import (
"encoding/base64"
"net/netip"
"os"
"path/filepath"
@@ -37,8 +38,17 @@ func TestValidateAppliesDefaultsAndParsedFields(t *testing.T) {
if cfg.Server.MaxHeaderBytes != 1<<20 {
t.Fatalf("MaxHeaderBytes = %d, want %d", cfg.Server.MaxHeaderBytes, 1<<20)
}
if cfg.Limits.DefaultTTLDuration != time.Hour {
t.Fatalf("DefaultTTLDuration = %s, want 1h", cfg.Limits.DefaultTTLDuration)
if !cfg.Server.AccessLog.Enabled {
t.Fatalf("AccessLog.Enabled = %v, want true", cfg.Server.AccessLog.Enabled)
}
if cfg.Server.AccessLog.MaxSizeBytes != 100*1024*1024 {
t.Fatalf("AccessLog.MaxSizeBytes = %d, want %d", cfg.Server.AccessLog.MaxSizeBytes, int64(100*1024*1024))
}
if cfg.Server.AccessLog.MaxBackups != 5 {
t.Fatalf("AccessLog.MaxBackups = %d, want 5", cfg.Server.AccessLog.MaxBackups)
}
if cfg.Limits.DefaultTTLDuration != 15*time.Minute {
t.Fatalf("DefaultTTLDuration = %s, want 15m", cfg.Limits.DefaultTTLDuration)
}
if cfg.Limits.RawCacheMaxBytes != 200*1024*1024 {
t.Fatalf("RawCacheMaxBytes = %d, want %d", cfg.Limits.RawCacheMaxBytes, int64(200*1024*1024))
@@ -49,6 +59,9 @@ func TestValidateAppliesDefaultsAndParsedFields(t *testing.T) {
if cfg.Storage.CleanupIntervalParsed != time.Minute {
t.Fatalf("CleanupIntervalParsed = %s, want 1m", cfg.Storage.CleanupIntervalParsed)
}
if len(cfg.Storage.EncryptionKeyBytes) != 32 {
t.Fatalf("EncryptionKeyBytes length = %d, want 32", len(cfg.Storage.EncryptionKeyBytes))
}
if len(cfg.Security.AllowedPrefixes) != 1 {
t.Fatalf("AllowedPrefixes len = %d, want 1", len(cfg.Security.AllowedPrefixes))
}
@@ -63,6 +76,7 @@ func TestLoadParsesConfiguredFields(t *testing.T) {
tmp := t.TempDir()
dataDir := filepath.Join(tmp, "store")
configPath := filepath.Join(tmp, "config.yaml")
keyPath := filepath.Join(tmp, "scratchbox.key")
content := `
server:
listen_addr: "127.0.0.1:9090"
@@ -71,6 +85,11 @@ server:
write_timeout: "35s"
idle_timeout: "90s"
max_header_bytes: 2097152
access_log:
enabled: true
file_path: "` + filepath.Join(tmp, "access.log") + `"
max_size: "2MiB"
max_backups: 7
limits:
max_upload_size: "2MiB"
default_ttl: "2h"
@@ -79,6 +98,7 @@ limits:
storage:
data_dir: "` + dataDir + `"
cleanup_interval: "45s"
encryption_key_file: "` + filepath.Base(keyPath) + `"
security:
allowed_ips:
- "127.0.0.1"
@@ -86,7 +106,15 @@ security:
trust_proxy_headers: true
trusted_proxy_ips:
- "10.0.0.0/8"
rate_limit:
rate_limit_ui:
enabled: true
requests_per_minute: 12
burst: 4
rate_limit_api_read:
enabled: true
requests_per_minute: 12
burst: 4
rate_limit_api_write:
enabled: true
requests_per_minute: 12
burst: 4
@@ -94,6 +122,9 @@ security:
if err := os.WriteFile(configPath, []byte(content), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
if err := os.WriteFile(keyPath, []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n"), 0o600); err != nil {
t.Fatalf("WriteFile(key) error = %v", err)
}
cfg, err := Load(configPath)
if err != nil {
@@ -118,6 +149,15 @@ security:
if cfg.Server.MaxHeaderBytes != 2*1024*1024 {
t.Fatalf("MaxHeaderBytes = %d, want %d", cfg.Server.MaxHeaderBytes, 2*1024*1024)
}
if !cfg.Server.AccessLog.Enabled {
t.Fatalf("AccessLog.Enabled = false, want true")
}
if cfg.Server.AccessLog.MaxSizeBytes != 2*1024*1024 {
t.Fatalf("AccessLog.MaxSizeBytes = %d, want %d", cfg.Server.AccessLog.MaxSizeBytes, int64(2*1024*1024))
}
if cfg.Server.AccessLog.MaxBackups != 7 {
t.Fatalf("AccessLog.MaxBackups = %d, want 7", cfg.Server.AccessLog.MaxBackups)
}
if cfg.Limits.MaxUploadSizeBytes != 2*1024*1024 {
t.Fatalf("MaxUploadSizeBytes = %d, want %d", cfg.Limits.MaxUploadSizeBytes, int64(2*1024*1024))
}
@@ -133,11 +173,23 @@ security:
if cfg.Storage.CleanupIntervalParsed != 45*time.Second {
t.Fatalf("CleanupIntervalParsed = %s, want 45s", cfg.Storage.CleanupIntervalParsed)
}
if len(cfg.Storage.EncryptionKeyBytes) != 32 {
t.Fatalf("EncryptionKeyBytes length = %d, want 32", len(cfg.Storage.EncryptionKeyBytes))
}
if cfg.Storage.EncryptionKeyBytes[0] != 0 {
t.Fatalf("EncryptionKeyBytes[0] = %d, want 0", cfg.Storage.EncryptionKeyBytes[0])
}
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 cfg.Security.RateLimitUI.RequestsPerMinute != 12 || cfg.Security.RateLimitUI.Burst != 4 {
t.Fatalf("unexpected rate_limit_ui values: %+v", cfg.Security.RateLimitUI)
}
if cfg.Security.RateLimitAPIRead.RequestsPerMinute != 12 || cfg.Security.RateLimitAPIRead.Burst != 4 {
t.Fatalf("unexpected rate_limit_api_read values: %+v", cfg.Security.RateLimitAPIRead)
}
if cfg.Security.RateLimitAPIWrite.RequestsPerMinute != 12 || cfg.Security.RateLimitAPIWrite.Burst != 4 {
t.Fatalf("unexpected rate_limit_api_write values: %+v", cfg.Security.RateLimitAPIWrite)
}
if len(cfg.Security.AllowedPrefixes) != 2 {
t.Fatalf("AllowedPrefixes len = %d, want 2", len(cfg.Security.AllowedPrefixes))
@@ -204,6 +256,20 @@ func TestValidateRejectsInvalidValues(t *testing.T) {
},
wantErr: "server.max_header_bytes",
},
{
name: "invalid access log max size",
mutate: func(cfg *Config) {
cfg.Server.AccessLog.MaxSize = "oops"
},
wantErr: "server.access_log.max_size",
},
{
name: "invalid access log max backups",
mutate: func(cfg *Config) {
cfg.Server.AccessLog.MaxBackups = 0
},
wantErr: "server.access_log.max_backups",
},
{
name: "invalid ttl parse",
mutate: func(cfg *Config) {
@@ -255,11 +321,25 @@ func TestValidateRejectsInvalidValues(t *testing.T) {
wantErr: "security.trusted_proxy_ips is required",
},
{
name: "invalid rate limit burst",
name: "invalid ui rate limit burst",
mutate: func(cfg *Config) {
cfg.Security.RateLimit.Burst = 0
cfg.Security.RateLimitUI.Burst = 0
},
wantErr: "security.rate_limit.burst",
wantErr: "security.rate_limit_ui.burst",
},
{
name: "invalid api read rate limit burst",
mutate: func(cfg *Config) {
cfg.Security.RateLimitAPIRead.Burst = 0
},
wantErr: "security.rate_limit_api_read.burst",
},
{
name: "invalid api write rate limit burst",
mutate: func(cfg *Config) {
cfg.Security.RateLimitAPIWrite.Burst = 0
},
wantErr: "security.rate_limit_api_write.burst",
},
{
name: "data dir required",
@@ -283,11 +363,39 @@ func TestValidateRejectsInvalidValues(t *testing.T) {
wantErr: "storage.cleanup_interval",
},
{
name: "invalid rate limit rpm",
name: "encryption key file required",
mutate: func(cfg *Config) {
cfg.Security.RateLimit.RequestsPerMinute = 0
cfg.Storage.EncryptionKeyFile = " "
},
wantErr: "security.rate_limit.requests_per_minute",
wantErr: "storage.encryption_key_file is required",
},
{
name: "encryption key bytes invalid length",
mutate: func(cfg *Config) {
cfg.Storage.EncryptionKeyBytes = []byte("short")
},
wantErr: "storage.encryption_key_file must decode to 32 bytes",
},
{
name: "invalid ui rate limit rpm",
mutate: func(cfg *Config) {
cfg.Security.RateLimitUI.RequestsPerMinute = 0
},
wantErr: "security.rate_limit_ui.requests_per_minute",
},
{
name: "invalid api read rate limit rpm",
mutate: func(cfg *Config) {
cfg.Security.RateLimitAPIRead.RequestsPerMinute = 0
},
wantErr: "security.rate_limit_api_read.requests_per_minute",
},
{
name: "invalid api write rate limit rpm",
mutate: func(cfg *Config) {
cfg.Security.RateLimitAPIWrite.RequestsPerMinute = 0
},
wantErr: "security.rate_limit_api_write.requests_per_minute",
},
}
@@ -328,6 +436,29 @@ func TestLoadErrors(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "parse config yaml") {
t.Fatalf("Load() error = %v, want yaml parse error", err)
}
validConfigPath := filepath.Join(tmp, "valid.yaml")
validConfig := `
storage:
data_dir: "` + filepath.Join(tmp, "store") + `"
cleanup_interval: "10s"
`
if err := os.WriteFile(validConfigPath, []byte(validConfig), 0o644); err != nil {
t.Fatalf("WriteFile(valid config) error = %v", err)
}
_, err = Load(validConfigPath)
if err == nil || !strings.Contains(err.Error(), "read storage.encryption_key_file") {
t.Fatalf("Load() error = %v, want missing key file error", err)
}
keyPath := filepath.Join(tmp, "config.key")
if err := os.WriteFile(keyPath, []byte("not-base64"), 0o600); err != nil {
t.Fatalf("WriteFile(invalid key) error = %v", err)
}
_, err = Load(validConfigPath)
if err == nil || !strings.Contains(err.Error(), "storage.encryption_key_file invalid: invalid base64") {
t.Fatalf("Load() error = %v, want invalid key base64 error", err)
}
}
func TestParseAllowedPrefixesTrimsAndParses(t *testing.T) {
@@ -385,3 +516,26 @@ func TestParseHumanSizeVariants(t *testing.T) {
})
}
}
func TestGenerateEncryptionKey(t *testing.T) {
t.Parallel()
k1, err := GenerateEncryptionKey()
if err != nil {
t.Fatalf("GenerateEncryptionKey() error = %v", err)
}
k2, err := GenerateEncryptionKey()
if err != nil {
t.Fatalf("GenerateEncryptionKey() second error = %v", err)
}
if k1 == k2 {
t.Fatalf("GenerateEncryptionKey() produced duplicate keys")
}
b1, err := base64.StdEncoding.DecodeString(k1)
if err != nil {
t.Fatalf("DecodeString(k1) error = %v", err)
}
if len(b1) != 32 {
t.Fatalf("decoded key length = %d, want 32", len(b1))
}
}