package config import ( "encoding/base64" "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*1024*1024 { t.Fatalf("MaxUploadSizeBytes = %d, want %d", cfg.Limits.MaxUploadSizeBytes, int64(100*1024*1024)) } if cfg.Server.ReadHeaderTimeoutDur != 5*time.Second { t.Fatalf("ReadHeaderTimeoutDur = %s, want 5s", cfg.Server.ReadHeaderTimeoutDur) } if cfg.Server.ReadTimeoutDur != 30*time.Second { t.Fatalf("ReadTimeoutDur = %s, want 30s", cfg.Server.ReadTimeoutDur) } if cfg.Server.WriteTimeoutDur != 30*time.Second { t.Fatalf("WriteTimeoutDur = %s, want 30s", cfg.Server.WriteTimeoutDur) } if cfg.Server.IdleTimeoutDur != 120*time.Second { t.Fatalf("IdleTimeoutDur = %s, want 120s", cfg.Server.IdleTimeoutDur) } if cfg.Server.MaxHeaderBytes != 1<<20 { t.Fatalf("MaxHeaderBytes = %d, want %d", cfg.Server.MaxHeaderBytes, 1<<20) } 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)) } if cfg.Limits.RawCacheMaxEntries != 32 { t.Fatalf("RawCacheMaxEntries = %d, want %d", cfg.Limits.RawCacheMaxEntries, 32) } 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)) } if got := cfg.Security.AllowedPrefixes[0]; got != netip.MustParsePrefix("127.0.0.1/32") { t.Fatalf("AllowedPrefixes[0] = %v, want 127.0.0.1/32", got) } } 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" read_header_timeout: "4s" read_timeout: "25s" 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" raw_cache_max_size: "16MiB" raw_cache_max_entries: 5 storage: data_dir: "` + dataDir + `" cleanup_interval: "45s" security: allowed_ips: - "127.0.0.1" - "10.0.0.0/8" trust_proxy_headers: true trusted_proxy_ips: - "10.0.0.0/8" 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 ` 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.Server.ReadHeaderTimeoutDur != 4*time.Second { t.Fatalf("ReadHeaderTimeoutDur = %s, want 4s", cfg.Server.ReadHeaderTimeoutDur) } if cfg.Server.ReadTimeoutDur != 25*time.Second { t.Fatalf("ReadTimeoutDur = %s, want 25s", cfg.Server.ReadTimeoutDur) } if cfg.Server.WriteTimeoutDur != 35*time.Second { t.Fatalf("WriteTimeoutDur = %s, want 35s", cfg.Server.WriteTimeoutDur) } if cfg.Server.IdleTimeoutDur != 90*time.Second { t.Fatalf("IdleTimeoutDur = %s, want 90s", cfg.Server.IdleTimeoutDur) } 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)) } if cfg.Limits.DefaultTTLDuration != 2*time.Hour { t.Fatalf("DefaultTTLDuration = %s, want 2h", cfg.Limits.DefaultTTLDuration) } if cfg.Limits.RawCacheMaxBytes != 16*1024*1024 { t.Fatalf("RawCacheMaxBytes = %d, want %d", cfg.Limits.RawCacheMaxBytes, int64(16*1024*1024)) } if cfg.Limits.RawCacheMaxEntries != 5 { t.Fatalf("RawCacheMaxEntries = %d, want %d", cfg.Limits.RawCacheMaxEntries, 5) } 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 _, err := os.Stat(filepath.Join(dataDir, dataDirEncryptionKeyFilename)); err != nil { t.Fatalf("expected data-dir key file to exist: %v", err) } if !cfg.Security.TrustProxyHeaders { t.Fatalf("TrustProxyHeaders = false, want true") } 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)) } if len(cfg.Security.TrustedProxyPrefixes) != 1 { t.Fatalf("TrustedProxyPrefixes len = %d, want 1", len(cfg.Security.TrustedProxyPrefixes)) } } 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 read header timeout", mutate: func(cfg *Config) { cfg.Server.ReadHeaderTimeout = "bogus" }, wantErr: "server.read_header_timeout", }, { name: "invalid read timeout", mutate: func(cfg *Config) { cfg.Server.ReadTimeout = "bogus" }, wantErr: "server.read_timeout", }, { name: "invalid write timeout", mutate: func(cfg *Config) { cfg.Server.WriteTimeout = "bogus" }, wantErr: "server.write_timeout", }, { name: "invalid idle timeout", mutate: func(cfg *Config) { cfg.Server.IdleTimeout = "bogus" }, wantErr: "server.idle_timeout", }, { name: "invalid max header bytes", mutate: func(cfg *Config) { cfg.Server.MaxHeaderBytes = 0 }, 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) { cfg.Limits.DefaultTTL = "not-a-duration" }, wantErr: "limits.default_ttl", }, { name: "invalid raw cache size", mutate: func(cfg *Config) { cfg.Limits.RawCacheMaxSize = "oops" }, wantErr: "limits.raw_cache_max_size", }, { name: "invalid raw cache max entries", mutate: func(cfg *Config) { cfg.Limits.RawCacheMaxEntries = 0 }, wantErr: "limits.raw_cache_max_entries", }, { 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 trusted proxy entry", mutate: func(cfg *Config) { cfg.Security.TrustedProxyIPs = []string{"not-an-ip"} }, wantErr: "security.trusted_proxy_ips", }, { name: "trust proxy headers requires trusted proxies", mutate: func(cfg *Config) { cfg.Security.TrustProxyHeaders = true cfg.Security.TrustedProxyIPs = nil }, wantErr: "security.trusted_proxy_ips is required", }, { name: "invalid ui rate limit burst", mutate: func(cfg *Config) { cfg.Security.RateLimitUI.Burst = 0 }, 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", 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 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", }, } 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) } 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 { t.Fatalf("Load() error = %v, want nil", err) } keyPath := filepath.Join(tmp, "store", dataDirEncryptionKeyFilename) 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 setup failed") { t.Fatalf("Load() error = %v, want invalid key base64 error", err) } } func TestLoadFailsWhenKeyMissingAndDataFilesExist(t *testing.T) { t.Parallel() tmp := t.TempDir() dataDir := filepath.Join(tmp, "store") scratchesDir := filepath.Join(dataDir, "scratches") if err := os.MkdirAll(scratchesDir, 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(filepath.Join(scratchesDir, "existing-scratch"), []byte("payload"), 0o600); err != nil { t.Fatalf("WriteFile() error = %v", err) } configPath := filepath.Join(tmp, "config.yaml") configRaw := ` storage: data_dir: "` + dataDir + `" cleanup_interval: "10s" ` if err := os.WriteFile(configPath, []byte(configRaw), 0o644); err != nil { t.Fatalf("WriteFile(config) error = %v", err) } _, err := Load(configPath) if err == nil || !strings.Contains(err.Error(), "refusing to generate missing") { t.Fatalf("Load() error = %v, want missing-key safety error", err) } } func TestLoadFromEnvParsesConfiguredFields(t *testing.T) { dataDir := filepath.Join(t.TempDir(), "store") t.Setenv("SCRATCHBOX_SERVER_LISTEN_ADDR", "127.0.0.1:19090") t.Setenv("SCRATCHBOX_SERVER_READ_HEADER_TIMEOUT", "6s") t.Setenv("SCRATCHBOX_SERVER_READ_TIMEOUT", "20s") t.Setenv("SCRATCHBOX_SERVER_WRITE_TIMEOUT", "40s") t.Setenv("SCRATCHBOX_SERVER_IDLE_TIMEOUT", "80s") t.Setenv("SCRATCHBOX_SERVER_MAX_HEADER_BYTES", "2097152") t.Setenv("SCRATCHBOX_SERVER_ACCESS_LOG_ENABLED", "false") t.Setenv("SCRATCHBOX_SERVER_ACCESS_LOG_FILE_PATH", "/tmp/access.log") t.Setenv("SCRATCHBOX_SERVER_ACCESS_LOG_MAX_SIZE", "4MiB") t.Setenv("SCRATCHBOX_SERVER_ACCESS_LOG_MAX_BACKUPS", "8") t.Setenv("SCRATCHBOX_LIMITS_MAX_UPLOAD_SIZE", "8MiB") t.Setenv("SCRATCHBOX_LIMITS_DEFAULT_TTL", "30m") t.Setenv("SCRATCHBOX_LIMITS_RAW_CACHE_MAX_SIZE", "64MiB") t.Setenv("SCRATCHBOX_LIMITS_RAW_CACHE_MAX_ENTRIES", "16") t.Setenv("SCRATCHBOX_STORAGE_DATA_DIR", dataDir) t.Setenv("SCRATCHBOX_STORAGE_CLEANUP_INTERVAL", "15s") t.Setenv("SCRATCHBOX_SECURITY_ALLOWED_IPS", "127.0.0.1,10.0.0.0/8") t.Setenv("SCRATCHBOX_SECURITY_TRUST_PROXY_HEADERS", "true") t.Setenv("SCRATCHBOX_SECURITY_TRUSTED_PROXY_IPS", "10.0.0.0/8") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_UI_ENABLED", "true") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_UI_REQUESTS_PER_MINUTE", "50") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_UI_BURST", "20") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_API_READ_ENABLED", "true") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_API_READ_REQUESTS_PER_MINUTE", "60") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_API_READ_BURST", "30") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_API_WRITE_ENABLED", "true") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_API_WRITE_REQUESTS_PER_MINUTE", "40") t.Setenv("SCRATCHBOX_SECURITY_RATE_LIMIT_API_WRITE_BURST", "12") cfg, err := LoadFromEnv() if err != nil { t.Fatalf("LoadFromEnv() error = %v", err) } if cfg.Server.ListenAddr != "127.0.0.1:19090" { t.Fatalf("ListenAddr = %q, want %q", cfg.Server.ListenAddr, "127.0.0.1:19090") } if cfg.Server.ReadHeaderTimeoutDur != 6*time.Second { t.Fatalf("ReadHeaderTimeoutDur = %s, want 6s", cfg.Server.ReadHeaderTimeoutDur) } if cfg.Server.ReadTimeoutDur != 20*time.Second { t.Fatalf("ReadTimeoutDur = %s, want 20s", cfg.Server.ReadTimeoutDur) } if cfg.Server.WriteTimeoutDur != 40*time.Second { t.Fatalf("WriteTimeoutDur = %s, want 40s", cfg.Server.WriteTimeoutDur) } if cfg.Server.IdleTimeoutDur != 80*time.Second { t.Fatalf("IdleTimeoutDur = %s, want 80s", cfg.Server.IdleTimeoutDur) } 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 = %v, want false", cfg.Server.AccessLog.Enabled) } if cfg.Limits.MaxUploadSizeBytes != 8*1024*1024 { t.Fatalf("MaxUploadSizeBytes = %d, want %d", cfg.Limits.MaxUploadSizeBytes, int64(8*1024*1024)) } if cfg.Limits.DefaultTTLDuration != 30*time.Minute { t.Fatalf("DefaultTTLDuration = %s, want 30m", cfg.Limits.DefaultTTLDuration) } if cfg.Limits.RawCacheMaxBytes != 64*1024*1024 { t.Fatalf("RawCacheMaxBytes = %d, want %d", cfg.Limits.RawCacheMaxBytes, int64(64*1024*1024)) } if cfg.Limits.RawCacheMaxEntries != 16 { t.Fatalf("RawCacheMaxEntries = %d, want 16", cfg.Limits.RawCacheMaxEntries) } if cfg.Storage.DataDir != dataDir { t.Fatalf("DataDir = %q, want %q", cfg.Storage.DataDir, dataDir) } if cfg.Storage.CleanupIntervalParsed != 15*time.Second { t.Fatalf("CleanupIntervalParsed = %s, want 15s", 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) != 2 { t.Fatalf("AllowedPrefixes len = %d, want 2", len(cfg.Security.AllowedPrefixes)) } if len(cfg.Security.TrustedProxyPrefixes) != 1 { t.Fatalf("TrustedProxyPrefixes len = %d, want 1", len(cfg.Security.TrustedProxyPrefixes)) } if !cfg.Security.TrustProxyHeaders { t.Fatalf("TrustProxyHeaders = false, want true") } if cfg.Security.RateLimitUI.RequestsPerMinute != 50 || cfg.Security.RateLimitUI.Burst != 20 { t.Fatalf("unexpected rate_limit_ui values: %+v", cfg.Security.RateLimitUI) } if cfg.Security.RateLimitAPIRead.RequestsPerMinute != 60 || cfg.Security.RateLimitAPIRead.Burst != 30 { t.Fatalf("unexpected rate_limit_api_read values: %+v", cfg.Security.RateLimitAPIRead) } if cfg.Security.RateLimitAPIWrite.RequestsPerMinute != 40 || cfg.Security.RateLimitAPIWrite.Burst != 12 { t.Fatalf("unexpected rate_limit_api_write values: %+v", cfg.Security.RateLimitAPIWrite) } } func TestLoadFromEnvRejectsInvalidBool(t *testing.T) { t.Setenv("SCRATCHBOX_STORAGE_DATA_DIR", filepath.Join(t.TempDir(), "store")) t.Setenv("SCRATCHBOX_SERVER_ACCESS_LOG_ENABLED", "not-bool") _, err := LoadFromEnv() if err == nil || !strings.Contains(err.Error(), "SCRATCHBOX_SERVER_ACCESS_LOG_ENABLED invalid bool") { t.Fatalf("LoadFromEnv() error = %v, want invalid bool env 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: "1TB", wantErr: "unsupported unit"}, {in: "1TiB", wantErr: "unsupported unit"}, {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) } }) } } 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)) } }