package config import ( "os" "path/filepath" "strings" "testing" "time" ) const ( testDriverPostgres = "postgres" testInvalid = "invalid" testLevelDebug = "debug" ) func upstreamConfigValues(upstream UpstreamConfig) map[string]string { return map[string]string{ "npm": upstream.NPM, "cargo": upstream.Cargo, "cargo_download": upstream.CargoDownload, "gem": upstream.Gem, "go": upstream.Go, "hex": upstream.Hex, "hex_api": upstream.HexAPI, "pub": upstream.Pub, "pypi": upstream.PyPI, "pypi_download": upstream.PyPIDownload, "maven": upstream.Maven, "gradle_plugin_portal": upstream.GradlePluginPortal, "nuget": upstream.NuGet, "nuget_search": upstream.NuGetSearch, "composer": upstream.Composer, "composer_repository": upstream.ComposerRepository, "conan": upstream.Conan, "conda": upstream.Conda, "cran": upstream.CRAN, "julia": upstream.Julia, "swift": upstream.Swift, "oci_default": upstream.OCIDefault, "debian": upstream.Debian, "rpm": upstream.RPM, "homebrew_api": upstream.HomebrewAPI, "homebrew_artifact": upstream.HomebrewArtifact, } } func defaultUpstreamValues() map[string]string { return map[string]string{ "npm": "https://registry.npmjs.org", "cargo": "https://index.crates.io", "cargo_download": "https://static.crates.io/crates", "gem": "https://rubygems.org", "go": "https://proxy.golang.org", "hex": "https://repo.hex.pm", "hex_api": "https://hex.pm", "pub": "https://pub.dev", "pypi": "https://pypi.org", "pypi_download": "https://files.pythonhosted.org", "maven": "https://repo1.maven.org/maven2", "gradle_plugin_portal": "https://plugins.gradle.org/m2", "nuget": "https://api.nuget.org", "nuget_search": "https://azuresearch-usnc.nuget.org", "composer": "https://packagist.org", "composer_repository": "https://repo.packagist.org", "conan": "https://center.conan.io", "conda": "https://conda.anaconda.org", "cran": "https://cloud.r-project.org", "julia": "https://pkg.julialang.org", "swift": "https://tuist.dev/api/registry/swift", "oci_default": "https://registry-1.docker.io", "debian": "http://deb.debian.org/debian", "rpm": "https://dl.fedoraproject.org/pub/fedora/linux", "homebrew_api": "https://formulae.brew.sh/api", "homebrew_artifact": "https://ghcr.io", } } func upstreamEnvironmentVariables() map[string]string { return map[string]string{ "npm": "PROXY_UPSTREAM_NPM", "cargo": "PROXY_UPSTREAM_CARGO", "cargo_download": "PROXY_UPSTREAM_CARGO_DOWNLOAD", "gem": "PROXY_UPSTREAM_GEM", "go": "PROXY_UPSTREAM_GO", "hex": "PROXY_UPSTREAM_HEX", "hex_api": "PROXY_UPSTREAM_HEX_API", "pub": "PROXY_UPSTREAM_PUB", "pypi": "PROXY_UPSTREAM_PYPI", "pypi_download": "PROXY_UPSTREAM_PYPI_DOWNLOAD", "maven": "PROXY_UPSTREAM_MAVEN", "gradle_plugin_portal": "PROXY_UPSTREAM_GRADLE_PLUGIN_PORTAL", "nuget": "PROXY_UPSTREAM_NUGET", "nuget_search": "PROXY_UPSTREAM_NUGET_SEARCH", "composer": "PROXY_UPSTREAM_COMPOSER", "composer_repository": "PROXY_UPSTREAM_COMPOSER_REPOSITORY", "conan": "PROXY_UPSTREAM_CONAN", "conda": "PROXY_UPSTREAM_CONDA", "cran": "PROXY_UPSTREAM_CRAN", "julia": "PROXY_UPSTREAM_JULIA", "swift": "PROXY_UPSTREAM_SWIFT", "oci_default": "PROXY_UPSTREAM_OCI_DEFAULT", "debian": "PROXY_UPSTREAM_DEBIAN", "rpm": "PROXY_UPSTREAM_RPM", "homebrew_api": "PROXY_UPSTREAM_HOMEBREW_API", "homebrew_artifact": "PROXY_UPSTREAM_HOMEBREW_ARTIFACT", } } func assertUpstreamValues(t *testing.T, cfg *Config, want map[string]string) { t.Helper() got := upstreamConfigValues(cfg.Upstream) for name, wantValue := range want { if gotValue := got[name]; gotValue != wantValue { t.Errorf("Upstream %s = %q, want %q", name, gotValue, wantValue) } } } func TestDefault(t *testing.T) { cfg := Default() if cfg.Listen != ":8080" { t.Errorf("Listen = %q, want %q", cfg.Listen, ":8080") } if cfg.Storage.Path == "" { t.Error("Storage.Path should not be empty") } if cfg.Database.Path == "" { t.Error("Database.Path should not be empty") } if cfg.AccessLog.Path != "" { t.Errorf("AccessLog.Path = %q, want disabled by default", cfg.AccessLog.Path) } if cfg.Gradle.BuildCache.MaxUploadSize != "100MB" { t.Errorf("Gradle.BuildCache.MaxUploadSize = %q, want %q", cfg.Gradle.BuildCache.MaxUploadSize, "100MB") } if cfg.Gradle.BuildCache.MaxAge != "168h" { t.Errorf("Gradle.BuildCache.MaxAge = %q, want %q", cfg.Gradle.BuildCache.MaxAge, "168h") } if len(cfg.Upstream.AllowPrivateHosts) != 0 { t.Errorf("Upstream.AllowPrivateHosts = %v, want empty", cfg.Upstream.AllowPrivateHosts) } if cfg.Upstream.AllowLoopback { t.Error("Upstream.AllowLoopback = true, want false") } assertUpstreamValues(t, cfg, defaultUpstreamValues()) } func TestValidate(t *testing.T) { tests := []struct { name string modify func(*Config) wantErr bool }{ { name: "valid default", modify: func(c *Config) {}, wantErr: false, }, { name: "empty listen", modify: func(c *Config) { c.Listen = "" }, wantErr: true, }, { name: "empty base_url", modify: func(c *Config) { c.BaseURL = "" }, wantErr: true, }, { name: "empty storage path and url", modify: func(c *Config) { c.Storage.Path = ""; c.Storage.URL = "" }, wantErr: true, }, { name: "storage url set", modify: func(c *Config) { c.Storage.Path = ""; c.Storage.URL = "s3://bucket" }, wantErr: false, }, { name: "empty database path for sqlite", modify: func(c *Config) { c.Database.Path = "" }, wantErr: true, }, { name: "invalid database driver", modify: func(c *Config) { c.Database.Driver = "mysql" }, wantErr: true, }, { name: "postgres without url", modify: func(c *Config) { c.Database.Driver = testDriverPostgres; c.Database.URL = "" }, wantErr: true, }, { name: "postgres with url", modify: func(c *Config) { c.Database.Driver = testDriverPostgres; c.Database.URL = "postgres://localhost/test" }, wantErr: false, }, { name: "invalid log level", modify: func(c *Config) { c.Log.Level = testInvalid }, wantErr: true, }, { name: "invalid log format", modify: func(c *Config) { c.Log.Format = testInvalid }, wantErr: true, }, { name: "invalid max size", modify: func(c *Config) { c.Storage.MaxSize = testInvalid }, wantErr: true, }, { name: "valid max size", modify: func(c *Config) { c.Storage.MaxSize = "10GB" }, wantErr: false, }, { name: "invalid gradle upload size", modify: func(c *Config) { c.Gradle.BuildCache.MaxUploadSize = testInvalid }, wantErr: true, }, { name: "zero gradle upload size", modify: func(c *Config) { c.Gradle.BuildCache.MaxUploadSize = "0" }, wantErr: true, }, { name: "invalid gradle max age", modify: func(c *Config) { c.Gradle.BuildCache.MaxAge = testInvalid }, wantErr: true, }, { name: "valid gradle max age", modify: func(c *Config) { c.Gradle.BuildCache.MaxAge = "24h" }, wantErr: false, }, { name: "invalid gradle max size", modify: func(c *Config) { c.Gradle.BuildCache.MaxSize = testInvalid }, wantErr: true, }, { name: "invalid gradle sweep interval", modify: func(c *Config) { c.Gradle.BuildCache.SweepInterval = "0" }, wantErr: true, }, { name: "valid gradle sweep interval", modify: func(c *Config) { c.Gradle.BuildCache.SweepInterval = "30m" }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.modify(cfg) err := cfg.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestParseSize(t *testing.T) { tests := []struct { input string want int64 wantErr bool }{ {"", 0, false}, {"0", 0, false}, {"100", 100, false}, {"1KB", 1024, false}, {"1K", 1024, false}, {"1MB", 1024 * 1024, false}, {"1M", 1024 * 1024, false}, {"1GB", 1024 * 1024 * 1024, false}, {"1G", 1024 * 1024 * 1024, false}, {"10GB", 10 * 1024 * 1024 * 1024, false}, {"1.5GB", int64(1.5 * 1024 * 1024 * 1024), false}, {"1TB", 1024 * 1024 * 1024 * 1024, false}, {"invalid", 0, true}, {"10XB", 0, true}, } for _, tt := range tests { got, err := ParseSize(tt.input) if (err != nil) != tt.wantErr { t.Errorf("ParseSize(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) continue } if got != tt.want { t.Errorf("ParseSize(%q) = %d, want %d", tt.input, got, tt.want) } } } func TestLoadYAML(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") content := ` listen: ":3000" base_url: "https://example.com" storage: path: "/data/cache" max_size: "5GB" database: path: "/data/proxy.db" log: level: "debug" format: "json" access_log: path: "/var/log/proxy/access.jsonl" upstream: homebrew_api: "https://homebrew-api.example.com" homebrew_artifact: "https://homebrew-artifact.example.com" ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("writing config file: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("Load failed: %v", err) } if cfg.Listen != ":3000" { t.Errorf("Listen = %q, want %q", cfg.Listen, ":3000") } if cfg.BaseURL != "https://example.com" { t.Errorf("BaseURL = %q, want %q", cfg.BaseURL, "https://example.com") } if cfg.Storage.Path != "/data/cache" { t.Errorf("Storage.Path = %q, want %q", cfg.Storage.Path, "/data/cache") } if cfg.Storage.MaxSize != "5GB" { t.Errorf("Storage.MaxSize = %q, want %q", cfg.Storage.MaxSize, "5GB") } if cfg.Log.Level != testLevelDebug { t.Errorf("Log.Level = %q, want %q", cfg.Log.Level, testLevelDebug) } if cfg.Log.Format != "json" { t.Errorf("Log.Format = %q, want %q", cfg.Log.Format, "json") } if cfg.AccessLog.Path != "/var/log/proxy/access.jsonl" { t.Errorf("AccessLog.Path = %q, want %q", cfg.AccessLog.Path, "/var/log/proxy/access.jsonl") } if cfg.Upstream.HomebrewAPI != "https://homebrew-api.example.com" { t.Errorf("Upstream.HomebrewAPI = %q, want %q", cfg.Upstream.HomebrewAPI, "https://homebrew-api.example.com") } if cfg.Upstream.HomebrewArtifact != "https://homebrew-artifact.example.com" { t.Errorf("Upstream.HomebrewArtifact = %q, want %q", cfg.Upstream.HomebrewArtifact, "https://homebrew-artifact.example.com") } } func TestLoadYAMLUpstreams(t *testing.T) { path := filepath.Join(t.TempDir(), "config.yaml") content := ` upstream: allow_private_hosts: - "registry.internal" - "10.0.0.12" allow_loopback: true npm: "https://upstream.example.com/npm" cargo: "https://upstream.example.com/cargo" cargo_download: "https://upstream.example.com/cargo_download" gem: "https://upstream.example.com/gem" go: "https://upstream.example.com/go" hex: "https://upstream.example.com/hex" hex_api: "https://upstream.example.com/hex_api" pub: "https://upstream.example.com/pub" pypi: "https://upstream.example.com/pypi" pypi_download: "https://upstream.example.com/pypi_download" maven: "https://upstream.example.com/maven" gradle_plugin_portal: "https://upstream.example.com/gradle_plugin_portal" nuget: "https://upstream.example.com/nuget" nuget_search: "https://upstream.example.com/nuget_search" composer: "https://upstream.example.com/composer" composer_repository: "https://upstream.example.com/composer_repository" conan: "https://upstream.example.com/conan" conda: "https://upstream.example.com/conda" cran: "https://upstream.example.com/cran" julia: "https://upstream.example.com/julia" swift: "https://upstream.example.com/swift" oci_default: "https://upstream.example.com/oci_default" debian: "https://upstream.example.com/debian" rpm: "https://upstream.example.com/rpm" homebrew_api: "https://upstream.example.com/homebrew_api" homebrew_artifact: "https://upstream.example.com/homebrew_artifact" ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("writing config file: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("Load failed: %v", err) } want := make(map[string]string) for name := range defaultUpstreamValues() { want[name] = "https://upstream.example.com/" + name } assertUpstreamValues(t, cfg, want) if got := strings.Join(cfg.Upstream.AllowPrivateHosts, ","); got != "registry.internal,10.0.0.12" { t.Errorf("Upstream.AllowPrivateHosts = %q, want %q", got, "registry.internal,10.0.0.12") } if !cfg.Upstream.AllowLoopback { t.Error("Upstream.AllowLoopback = false, want true") } } func TestLoadJSON(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.json") content := `{ "listen": ":4000", "base_url": "https://json.example.com", "upstream": { "gem": "https://json.example.com/gem", "rpm": "https://json.example.com/rpm" } }` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("writing config file: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("Load failed: %v", err) } if cfg.Listen != ":4000" { t.Errorf("Listen = %q, want %q", cfg.Listen, ":4000") } if cfg.BaseURL != "https://json.example.com" { t.Errorf("BaseURL = %q, want %q", cfg.BaseURL, "https://json.example.com") } if cfg.Upstream.Gem != "https://json.example.com/gem" { t.Errorf("Upstream.Gem = %q, want %q", cfg.Upstream.Gem, "https://json.example.com/gem") } if cfg.Upstream.RPM != "https://json.example.com/rpm" { t.Errorf("Upstream.RPM = %q, want %q", cfg.Upstream.RPM, "https://json.example.com/rpm") } } func TestLoadFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_LISTEN", ":9000") t.Setenv("PROXY_BASE_URL", "https://env.example.com") t.Setenv("PROXY_UI_URL", "https://ui.env.example.com/ui") t.Setenv("PROXY_STORAGE_PATH", "/env/cache") t.Setenv("PROXY_LOG_LEVEL", testLevelDebug) t.Setenv("PROXY_ACCESS_LOG_PATH", "/tmp/proxy-access.jsonl") t.Setenv("PROXY_TRUST_FORWARDED_FOR", "true") t.Setenv("PROXY_UI_REQUEST_SOURCES", "true") t.Setenv("PROXY_UI_FAILURE_DETAILS", "true") t.Setenv("PROXY_UPSTREAM_ALLOW_PRIVATE_HOSTS", "registry.internal, 10.0.0.12") t.Setenv("PROXY_UPSTREAM_ALLOW_LOOPBACK", "true") t.Setenv("PROXY_GRADLE_BUILD_CACHE_READ_ONLY", "true") t.Setenv("PROXY_GRADLE_BUILD_CACHE_MAX_UPLOAD_SIZE", "32MB") t.Setenv("PROXY_GRADLE_BUILD_CACHE_MAX_AGE", "12h") t.Setenv("PROXY_GRADLE_BUILD_CACHE_MAX_SIZE", "10GB") t.Setenv("PROXY_GRADLE_BUILD_CACHE_SWEEP_INTERVAL", "15m") cfg.LoadFromEnv() if cfg.Listen != ":9000" { t.Errorf("Listen = %q, want %q", cfg.Listen, ":9000") } if cfg.BaseURL != "https://env.example.com" { t.Errorf("BaseURL = %q, want %q", cfg.BaseURL, "https://env.example.com") } if cfg.UIBaseURL != "https://ui.env.example.com/ui" { t.Errorf("UIBaseURL = %q, want %q", cfg.UIBaseURL, "https://ui.env.example.com/ui") } if cfg.Storage.Path != "/env/cache" { t.Errorf("Storage.Path = %q, want %q", cfg.Storage.Path, "/env/cache") } if cfg.Log.Level != testLevelDebug { t.Errorf("Log.Level = %q, want %q", cfg.Log.Level, testLevelDebug) } if cfg.AccessLog.Path != "/tmp/proxy-access.jsonl" { t.Errorf("AccessLog.Path = %q, want %q", cfg.AccessLog.Path, "/tmp/proxy-access.jsonl") } // Containers configure the proxy through the environment, so a setting // reachable only from YAML is unreachable in the deployment that most needs // it -- which is the one sitting behind an ingress. if !cfg.TrustForwardedFor { t.Error("TrustForwardedFor = false, want true from the environment") } if !cfg.UIRequestSources { t.Error("UIRequestSources = false, want true from the environment") } if !cfg.UIFailureDetails { t.Error("UIFailureDetails = false, want true from the environment") } if got := strings.Join(cfg.Upstream.AllowPrivateHosts, ","); got != "registry.internal,10.0.0.12" { t.Errorf("Upstream.AllowPrivateHosts = %q, want %q", got, "registry.internal,10.0.0.12") } if !cfg.Upstream.AllowLoopback { t.Error("Upstream.AllowLoopback = false, want true") } t.Setenv("PROXY_UPSTREAM_ALLOW_PRIVATE_HOSTS", " , ") cfg.LoadFromEnv() if got := strings.Join(cfg.Upstream.AllowPrivateHosts, ","); got != "registry.internal,10.0.0.12" { t.Errorf("Upstream.AllowPrivateHosts after empty env = %q, want unchanged", got) } if !cfg.Gradle.BuildCache.ReadOnly { t.Error("Gradle.BuildCache.ReadOnly = false, want true") } if cfg.Gradle.BuildCache.MaxUploadSize != "32MB" { t.Errorf("Gradle.BuildCache.MaxUploadSize = %q, want %q", cfg.Gradle.BuildCache.MaxUploadSize, "32MB") } if cfg.Gradle.BuildCache.MaxAge != "12h" { t.Errorf("Gradle.BuildCache.MaxAge = %q, want %q", cfg.Gradle.BuildCache.MaxAge, "12h") } if cfg.Gradle.BuildCache.MaxSize != "10GB" { t.Errorf("Gradle.BuildCache.MaxSize = %q, want %q", cfg.Gradle.BuildCache.MaxSize, "10GB") } if cfg.Gradle.BuildCache.SweepInterval != "15m" { t.Errorf("Gradle.BuildCache.SweepInterval = %q, want %q", cfg.Gradle.BuildCache.SweepInterval, "15m") } } func TestLoadFromEnvUpstreams(t *testing.T) { cfg := Default() want := make(map[string]string) for name, envName := range upstreamEnvironmentVariables() { value := "https://env.example.com/" + name t.Setenv(envName, value) want[name] = value } cfg.LoadFromEnv() assertUpstreamValues(t, cfg, want) } func TestLoadCooldownConfig(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.yaml") content := ` listen: ":8080" base_url: "http://localhost:8080" storage: path: "/data/cache" database: path: "/data/proxy.db" cooldown: default: "3d" ecosystems: npm: "7d" cargo: "0" packages: "pkg:npm/lodash": "0" "pkg:npm/@babel/core": "14d" package_patterns: "pkg:npm/@example/*": "0" ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("writing config file: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("Load failed: %v", err) } if cfg.Cooldown.Default != "3d" { t.Errorf("Cooldown.Default = %q, want %q", cfg.Cooldown.Default, "3d") } if cfg.Cooldown.Ecosystems["npm"] != "7d" { t.Errorf("Cooldown.Ecosystems[npm] = %q, want %q", cfg.Cooldown.Ecosystems["npm"], "7d") } if cfg.Cooldown.Ecosystems["cargo"] != "0" { t.Errorf("Cooldown.Ecosystems[cargo] = %q, want %q", cfg.Cooldown.Ecosystems["cargo"], "0") } if cfg.Cooldown.Packages["pkg:npm/lodash"] != "0" { t.Errorf("Cooldown.Packages[lodash] = %q, want %q", cfg.Cooldown.Packages["pkg:npm/lodash"], "0") } if cfg.Cooldown.Packages["pkg:npm/@babel/core"] != "14d" { t.Errorf("Cooldown.Packages[@babel/core] = %q, want %q", cfg.Cooldown.Packages["pkg:npm/@babel/core"], "14d") } if got := cfg.Cooldown.NormalizedPackages()["pkg:npm/%40babel/core"]; got != "14d" { t.Errorf("normalized Cooldown.Packages[@babel/core] = %q, want %q", got, "14d") } if cfg.Cooldown.PackagePatterns["pkg:npm/@example/*"] != "0" { t.Errorf("Cooldown.PackagePatterns[example] = %q, want %q", cfg.Cooldown.PackagePatterns["pkg:npm/@example/*"], "0") } } func TestCooldownConfigNormalizedPackages(t *testing.T) { rawScoped := "pkg:npm/@typescript/typescript-darwin-arm64" canonicalScoped := "pkg:npm/%40typescript/typescript-darwin-arm64" cfg := CooldownConfig{Packages: map[string]string{ rawScoped: "2d", canonicalScoped: "3d", "not-a-purl": "4d", }} got := cfg.NormalizedPackages() if got[canonicalScoped] != "3d" { t.Errorf("canonical scoped package duration = %q, want %q", got[canonicalScoped], "3d") } if _, exists := got[rawScoped]; exists { t.Errorf("raw scoped package key %q was not canonicalized", rawScoped) } if got["not-a-purl"] != "4d" { t.Errorf("invalid PURL duration = %q, want preserved value %q", got["not-a-purl"], "4d") } if cfg.Packages[rawScoped] != "2d" { t.Error("NormalizedPackages mutated the source map") } } func TestLoadCooldownFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_COOLDOWN_DEFAULT", "5d") cfg.LoadFromEnv() if cfg.Cooldown.Default != "5d" { t.Errorf("Cooldown.Default = %q, want %q", cfg.Cooldown.Default, "5d") } } func TestLoadFileNotFound(t *testing.T) { _, err := Load("/nonexistent/config.yaml") if err == nil { t.Error("expected error for nonexistent file") } } func TestParseMaxSize(t *testing.T) { tests := []struct { name string maxSize string want int64 }{ {"empty means unlimited", "", 0}, {"zero means unlimited", "0", 0}, {"10GB", "10GB", 10 * 1024 * 1024 * 1024}, {"500MB", "500MB", 500 * 1024 * 1024}, {"invalid returns 0", "invalid", 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.Storage.MaxSize = tt.maxSize got := cfg.ParseMaxSize() if got != tt.want { t.Errorf("ParseMaxSize() = %d, want %d", got, tt.want) } }) } } func TestParseMetadataTTL(t *testing.T) { tests := []struct { name string ttl string want time.Duration }{ {"empty defaults to 5m", "", 5 * time.Minute}, {"explicit zero", "0", 0}, {"10 minutes", "10m", 10 * time.Minute}, {"1 hour", "1h", 1 * time.Hour}, {"invalid defaults to 5m", "not-a-duration", 5 * time.Minute}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.MetadataTTL = tt.ttl got := cfg.ParseMetadataTTL() if got != tt.want { t.Errorf("ParseMetadataTTL() = %v, want %v", got, tt.want) } }) } } func TestParseMetadataMaxSize(t *testing.T) { tests := []struct { name string size string want int64 }{ {"unset uses default", "", defaultMetadataMaxSize}, {"explicit value", "250MB", 250 << 20}, {"bytes", "1024", 1024}, {"invalid uses default", "lots", defaultMetadataMaxSize}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.MetadataMaxSize = tt.size got := cfg.ParseMetadataMaxSize() if got != tt.want { t.Errorf("ParseMetadataMaxSize() = %d, want %d", got, tt.want) } }) } } func TestValidateMetadataMaxSize(t *testing.T) { cfg := Default() cfg.MetadataMaxSize = "not-a-size" if err := cfg.Validate(); err == nil { t.Error("expected validation error for invalid metadata_max_size") } cfg.MetadataMaxSize = "0" if err := cfg.Validate(); err == nil { t.Error("expected validation error for zero metadata_max_size") } cfg.MetadataMaxSize = "250MB" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for valid metadata_max_size: %v", err) } cfg.MetadataMaxSize = "" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for unset metadata_max_size: %v", err) } } func TestValidateMetadataTTL(t *testing.T) { cfg := Default() cfg.MetadataTTL = "invalid" if err := cfg.Validate(); err == nil { t.Error("expected validation error for invalid metadata_ttl") } cfg.MetadataTTL = "5m" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for valid metadata_ttl: %v", err) } cfg.MetadataTTL = "0" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for zero metadata_ttl: %v", err) } } func TestValidateHealthStorageProbeInterval(t *testing.T) { cfg := Default() cfg.Health.StorageProbeInterval = "not-a-duration" if err := cfg.Validate(); err == nil { t.Error("expected validation error for invalid health.storage_probe_interval") } cfg.Health.StorageProbeInterval = "30s" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for valid health.storage_probe_interval: %v", err) } cfg.Health.StorageProbeInterval = "0" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for zero health.storage_probe_interval: %v", err) } cfg.Health.StorageProbeInterval = "" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for empty health.storage_probe_interval: %v", err) } cfg.Health.StorageProbeInterval = "-5s" if err := cfg.Validate(); err == nil { t.Error("expected validation error for negative health.storage_probe_interval") } } func TestScanningConfigValidate(t *testing.T) { tests := []struct { name string cfg ScanningConfig wantErr bool }{ { name: "disabled skips validation entirely", cfg: ScanningConfig{Enabled: false, Timeout: "not-a-duration"}, }, { name: "enabled without signing key fails", cfg: ScanningConfig{Enabled: true}, wantErr: true, }, { name: "enabled with signing key and no scanners fails", cfg: ScanningConfig{Enabled: true, SigningKey: "s3cret"}, wantErr: true, }, { name: "invalid fetch_base_url fails", cfg: ScanningConfig{ Enabled: true, SigningKey: "s3cret", FetchBaseURL: "not-a-url", Scanners: []ScannerConfig{{Name: "clamav", URL: "http://scanner.invalid/scan"}}, }, wantErr: true, }, { name: "invalid timeout fails", cfg: ScanningConfig{ Enabled: true, SigningKey: "s3cret", Timeout: "not-a-duration", Scanners: []ScannerConfig{{Name: "clamav", URL: "http://scanner.invalid/scan"}}, }, wantErr: true, }, { name: "zero timeout fails", cfg: ScanningConfig{ Enabled: true, SigningKey: "s3cret", Timeout: "0", Scanners: []ScannerConfig{{Name: "clamav", URL: "http://scanner.invalid/scan"}}, }, wantErr: true, }, { name: "empty timeout defaults and is valid", cfg: ScanningConfig{ Enabled: true, SigningKey: "s3cret", Timeout: "", Scanners: []ScannerConfig{{Name: "clamav", URL: "http://scanner.invalid/scan"}}, }, }, { name: "scanner missing name fails", cfg: ScanningConfig{ Enabled: true, SigningKey: "s3cret", Scanners: []ScannerConfig{{URL: "http://scanner.invalid/scan"}}, }, wantErr: true, }, { name: "scanner with invalid url fails", cfg: ScanningConfig{ Enabled: true, SigningKey: "s3cret", Scanners: []ScannerConfig{{Name: "clamav", URL: "not-a-url"}}, }, wantErr: true, }, { name: "scanner with invalid mode fails", cfg: ScanningConfig{ Enabled: true, SigningKey: "s3cret", Scanners: []ScannerConfig{ {Name: "clamav", URL: "http://scanner.invalid/scan", Mode: "quarantine"}, }, }, wantErr: true, }, { name: "scanner with default mode is valid", cfg: ScanningConfig{ Enabled: true, SigningKey: "s3cret", Scanners: []ScannerConfig{{Name: "clamav", URL: "http://scanner.invalid/scan"}}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.cfg.Validate() if tt.wantErr && err == nil { t.Error("Validate() error = nil, want error") } if !tt.wantErr && err != nil { t.Errorf("Validate() unexpected error: %v", err) } }) } } func TestParseHTTPTimeout(t *testing.T) { tests := []struct { name string timeout string want time.Duration }{ {"empty defaults to 30s", "", 30 * time.Second}, {"explicit zero disables", "0", 0}, {"2 minutes", "2m", 2 * time.Minute}, {"90 seconds", "90s", 90 * time.Second}, {"invalid defaults to 30s", "not-a-duration", 30 * time.Second}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.HTTPTimeout = tt.timeout got := cfg.ParseHTTPTimeout() if got != tt.want { t.Errorf("ParseHTTPTimeout() = %v, want %v", got, tt.want) } }) } } func TestValidateHTTPTimeout(t *testing.T) { cfg := Default() cfg.HTTPTimeout = "not-a-duration" if err := cfg.Validate(); err == nil { t.Error("expected validation error for invalid http_timeout") } cfg.HTTPTimeout = "-5s" if err := cfg.Validate(); err == nil { t.Error("expected validation error for negative http_timeout") } cfg.HTTPTimeout = "2m" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for valid http_timeout: %v", err) } cfg.HTTPTimeout = "0" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for zero http_timeout: %v", err) } cfg.HTTPTimeout = "" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for empty http_timeout: %v", err) } } func TestParseMetadataRewriteCacheSize(t *testing.T) { tests := []struct { name string size string want int64 }{ {"empty defaults to 256MB", "", 256 << 20}, {"explicit zero disables", "0", 0}, {"1GB", "1GB", 1 << 30}, {"invalid defaults to 256MB", "lots", 256 << 20}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.MetadataRewriteCacheSize = tt.size if got := cfg.ParseMetadataRewriteCacheSize(); got != tt.want { t.Errorf("ParseMetadataRewriteCacheSize() = %d, want %d", got, tt.want) } }) } } func TestValidateMetadataRewriteCacheSize(t *testing.T) { cfg := Default() cfg.MetadataRewriteCacheSize = "lots" if err := cfg.Validate(); err == nil { t.Error("expected validation error for invalid metadata_rewrite_cache_size") } for _, ok := range []string{"512MB", "0", ""} { cfg.MetadataRewriteCacheSize = ok if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for metadata_rewrite_cache_size %q: %v", ok, err) } } } func TestLoadMetadataRewriteCacheSizeFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_METADATA_REWRITE_CACHE_SIZE", "1GB") cfg.LoadFromEnv() if cfg.MetadataRewriteCacheSize != "1GB" { t.Errorf("MetadataRewriteCacheSize = %q, want %q", cfg.MetadataRewriteCacheSize, "1GB") } } func TestLoadHTTPTimeoutFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_HTTP_TIMEOUT", "90s") cfg.LoadFromEnv() if cfg.HTTPTimeout != "90s" { t.Errorf("HTTPTimeout = %q, want %q", cfg.HTTPTimeout, "90s") } } func TestParseHitFlushInterval(t *testing.T) { tests := []struct { name string interval string want time.Duration }{ {"empty defaults to 1s", "", time.Second}, {"explicit zero disables", "0", 0}, {"10 seconds", "10s", 10 * time.Second}, {"invalid defaults to 1s", "not-a-duration", time.Second}, {"negative defaults to 1s", "-5s", time.Second}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.Database.HitFlushInterval = tt.interval got := cfg.ParseHitFlushInterval() if got != tt.want { t.Errorf("ParseHitFlushInterval() = %v, want %v", got, tt.want) } }) } } func TestValidateHitFlushInterval(t *testing.T) { cfg := Default() cfg.Database.HitFlushInterval = "not-a-duration" if err := cfg.Validate(); err == nil { t.Error("expected validation error for invalid hit_flush_interval") } cfg.Database.HitFlushInterval = "-5s" if err := cfg.Validate(); err == nil { t.Error("expected validation error for negative hit_flush_interval") } for _, ok := range []string{"10s", "0", ""} { cfg.Database.HitFlushInterval = ok if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for hit_flush_interval %q: %v", ok, err) } } } func TestLoadHitFlushIntervalFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_DATABASE_HIT_FLUSH_INTERVAL", "10s") cfg.LoadFromEnv() if cfg.Database.HitFlushInterval != "10s" { t.Errorf("HitFlushInterval = %q, want %q", cfg.Database.HitFlushInterval, "10s") } } func TestLoadMetadataTTLFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_METADATA_TTL", "10m") cfg.LoadFromEnv() if cfg.MetadataTTL != "10m" { t.Errorf("MetadataTTL = %q, want %q", cfg.MetadataTTL, "10m") } } func TestParseGradleBuildCacheConfig(t *testing.T) { cfg := Default() if got := cfg.ParseGradleBuildCacheMaxUploadSize(); got != 100*1024*1024 { t.Errorf("ParseGradleBuildCacheMaxUploadSize() = %d, want %d", got, 100*1024*1024) } if got := cfg.ParseGradleBuildCacheMaxAge(); got != 168*time.Hour { t.Errorf("ParseGradleBuildCacheMaxAge() = %v, want %v", got, 168*time.Hour) } if got := cfg.ParseGradleBuildCacheMaxSize(); got != 0 { t.Errorf("ParseGradleBuildCacheMaxSize() = %d, want 0", got) } if got := cfg.ParseGradleBuildCacheSweepInterval(); got != 10*time.Minute { t.Errorf("ParseGradleBuildCacheSweepInterval() = %v, want %v", got, 10*time.Minute) } cfg.Gradle.BuildCache.MaxUploadSize = "64MB" cfg.Gradle.BuildCache.MaxAge = "48h" cfg.Gradle.BuildCache.MaxSize = "2GB" cfg.Gradle.BuildCache.SweepInterval = "20m" if got := cfg.ParseGradleBuildCacheMaxUploadSize(); got != 64*1024*1024 { t.Errorf("ParseGradleBuildCacheMaxUploadSize() = %d, want %d", got, 64*1024*1024) } if got := cfg.ParseGradleBuildCacheMaxAge(); got != 48*time.Hour { t.Errorf("ParseGradleBuildCacheMaxAge() = %v, want %v", got, 48*time.Hour) } if got := cfg.ParseGradleBuildCacheMaxSize(); got != 2*1024*1024*1024 { t.Errorf("ParseGradleBuildCacheMaxSize() = %d, want %d", got, 2*1024*1024*1024) } if got := cfg.ParseGradleBuildCacheSweepInterval(); got != 20*time.Minute { t.Errorf("ParseGradleBuildCacheSweepInterval() = %v, want %v", got, 20*time.Minute) } } func TestParseDirectServeTTL(t *testing.T) { tests := []struct { name string ttl string want time.Duration }{ {"empty defaults to 15m", "", 15 * time.Minute}, {"5 minutes", "5m", 5 * time.Minute}, {"1 hour", "1h", 1 * time.Hour}, {"invalid defaults to 15m", "not-a-duration", 15 * time.Minute}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.Storage.DirectServeTTL = tt.ttl got := cfg.ParseDirectServeTTL() if got != tt.want { t.Errorf("ParseDirectServeTTL() = %v, want %v", got, tt.want) } }) } } func TestValidateDirectServeTTL(t *testing.T) { cfg := Default() cfg.Storage.DirectServeTTL = "invalid" if err := cfg.Validate(); err == nil { t.Error("expected validation error for invalid storage.direct_serve_ttl") } cfg.Storage.DirectServeTTL = "5m" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for valid storage.direct_serve_ttl: %v", err) } } func TestLoadDirectServeFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_STORAGE_DIRECT_SERVE", "true") t.Setenv("PROXY_STORAGE_DIRECT_SERVE_TTL", "30m") t.Setenv("PROXY_STORAGE_DIRECT_SERVE_BASE_URL", "https://cdn.example.com") cfg.LoadFromEnv() if !cfg.Storage.DirectServe { t.Error("Storage.DirectServe should be true") } if cfg.Storage.DirectServeTTL != "30m" { t.Errorf("Storage.DirectServeTTL = %q, want %q", cfg.Storage.DirectServeTTL, "30m") } if cfg.Storage.DirectServeBaseURL != "https://cdn.example.com" { t.Errorf("Storage.DirectServeBaseURL = %q, want %q", cfg.Storage.DirectServeBaseURL, "https://cdn.example.com") } } func TestValidateUIBaseURLDefaultsToBaseURL(t *testing.T) { cfg := Default() cfg.BaseURL = "https://proxy.example.com" cfg.UIBaseURL = "" if err := cfg.Validate(); err != nil { t.Fatalf("unexpected validation error: %v", err) } if cfg.UIBaseURL != "https://proxy.example.com" { t.Errorf("UIBaseURL = %q, want it to default to BaseURL %q", cfg.UIBaseURL, "https://proxy.example.com") } } func TestValidateUIBaseURL(t *testing.T) { cfg := Default() cfg.UIBaseURL = "not a url" if err := cfg.Validate(); err == nil { t.Error("expected validation error for relative ui_base_url") } cfg = Default() cfg.UIBaseURL = "://bad" if err := cfg.Validate(); err == nil { t.Error("expected validation error for unparseable ui_base_url") } cfg = Default() cfg.UIBaseURL = "https://ui.example.com/ui" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for valid ui_base_url: %v", err) } } func TestValidateDirectServeBaseURL(t *testing.T) { cfg := Default() cfg.Storage.DirectServeBaseURL = "not a url" if err := cfg.Validate(); err == nil { t.Error("expected validation error for relative direct_serve_base_url") } cfg.Storage.DirectServeBaseURL = "://bad" if err := cfg.Validate(); err == nil { t.Error("expected validation error for unparseable direct_serve_base_url") } cfg.Storage.DirectServeBaseURL = "https://cdn.example.com" if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for valid direct_serve_base_url: %v", err) } } func TestDatabaseConfigString(t *testing.T) { tests := []struct { name string cfg DatabaseConfig want string }{ {"sqlite", DatabaseConfig{Driver: "sqlite", Path: "./cache/proxy.db"}, "./cache/proxy.db"}, {"default driver", DatabaseConfig{Path: "/var/lib/proxy.db"}, "/var/lib/proxy.db"}, {"postgres no password", DatabaseConfig{Driver: "postgres", URL: "postgres://user@localhost:5432/proxy"}, "postgres://user@localhost:5432/proxy"}, {"postgres redacts password", DatabaseConfig{Driver: "postgres", URL: "postgres://user:secret@localhost:5432/proxy?sslmode=disable"}, "postgres://user:xxxxx@localhost:5432/proxy?sslmode=disable"}, {"postgres unparseable url", DatabaseConfig{Driver: "postgres", URL: "host=localhost user=foo password=bar"}, "postgres"}, {"postgres ignores sqlite path", DatabaseConfig{Driver: "postgres", URL: "postgres://localhost/db", Path: "./cache/proxy.db"}, "postgres://localhost/db"}, } for _, tt := range tests { if got := tt.cfg.String(); got != tt.want { t.Errorf("%s: String() = %q, want %q", tt.name, got, tt.want) } } } func TestUpstreamAuthForURLMatchesURLComponents(t *testing.T) { registryAuth := AuthConfig{Type: "bearer", Token: "registry-token"} privateAuth := AuthConfig{Type: "bearer", Token: "private-token"} config := UpstreamConfig{Auth: map[string]AuthConfig{ "https://registry.example.com": registryAuth, "https://registry.example.com/private": privateAuth, }} tests := []struct { name string url string wantToken string }{ {name: "registry root", url: "https://registry.example.com/package", wantToken: "registry-token"}, {name: "host is case insensitive", url: "https://REGISTRY.EXAMPLE.COM/package", wantToken: "registry-token"}, {name: "longest path match", url: "https://registry.example.com/private/package", wantToken: "private-token"}, {name: "exact path match", url: "https://registry.example.com/private", wantToken: "private-token"}, {name: "path segment boundary", url: "https://registry.example.com/private-other/package", wantToken: "registry-token"}, {name: "lookalike host rejected", url: "https://registry.example.com.evil.test/package"}, {name: "different scheme rejected", url: "http://registry.example.com/package"}, {name: "different port rejected", url: "https://registry.example.com:8443/package"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { auth := config.AuthForURL(tt.url) if tt.wantToken == "" { if auth != nil { t.Fatalf("AuthForURL() = %+v, want nil", auth) } return } if auth == nil { t.Fatal("AuthForURL() = nil, want authentication") } if auth.Token != tt.wantToken { t.Errorf("token = %q, want %q", auth.Token, tt.wantToken) } }) } } func TestValidateUpstreamAuthURLs(t *testing.T) { t.Run("valid absolute URL", func(t *testing.T) { cfg := Default() cfg.Upstream.Auth = map[string]AuthConfig{ "https://registry.example.com/private": {Type: "bearer", Token: "token"}, } if err := cfg.Validate(); err != nil { t.Fatalf("Validate() error = %v", err) } }) t.Run("invalid URL", func(t *testing.T) { cfg := Default() cfg.Upstream.Auth = map[string]AuthConfig{ "registry.example.com": {Type: "bearer", Token: "token"}, } err := cfg.Validate() if err == nil { t.Fatal("Validate() error = nil, want invalid upstream.auth URL error") } if !strings.Contains(err.Error(), "upstream.auth") || !strings.Contains(err.Error(), "registry.example.com") { t.Errorf("Validate() error = %q, want field and URL", err) } }) } func TestValidatePackageRoutes(t *testing.T) { tests := []struct { name string modify func(*Config) wantErr bool }{ { name: "valid Composer and npm routes", modify: func(cfg *Config) { cfg.Upstream.ComposerRoutes = map[string]string{"acme/*": "https://composer.example.com/packages"} cfg.Upstream.NPMRoutes = map[string]string{"@acme/*": "https://npm.example.com/registry"} }, }, { name: "Composer route URL is not absolute", modify: func(cfg *Config) { cfg.Upstream.ComposerRoutes = map[string]string{"acme/*": "composer.example.com"} }, wantErr: true, }, { name: "npm route URL is not absolute", modify: func(cfg *Config) { cfg.Upstream.NPMRoutes = map[string]string{"@acme/*": "npm.example.com"} }, wantErr: true, }, { name: "Composer route pattern is malformed", modify: func(cfg *Config) { cfg.Upstream.ComposerRoutes = map[string]string{"acme/[": "https://composer.example.com"} }, wantErr: true, }, { name: "npm route pattern is empty", modify: func(cfg *Config) { cfg.Upstream.NPMRoutes = map[string]string{"": "https://npm.example.com"} }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.modify(cfg) err := cfg.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %t", err, tt.wantErr) } }) } } func TestValidateNamedUpstreams(t *testing.T) { tests := []struct { name string modify func(*Config) wantErr bool }{ { name: "valid Helm, OCI, APK, Arch, and generic upstreams", modify: func(cfg *Config) { cfg.Upstream.Helm = map[string]string{"bitnami": "https://charts.bitnami.com/bitnami"} cfg.Upstream.OCI = map[string]string{"ghcr": "https://ghcr.io"} cfg.Upstream.APK = map[string]string{"alpine": "https://dl-cdn.alpinelinux.org/alpine"} cfg.Upstream.Arch = map[string]string{ "archlinux": "https://geo.mirror.pkgbuild.com", "archlinuxarm": "https://de3.mirror.archlinuxarm.org", } cfg.Upstream.Generic = map[string]string{ "github": "https://github.com", "github-api": "https://api.github.com", } }, }, { name: "generic upstream name contains path separator", modify: func(cfg *Config) { cfg.Upstream.Generic = map[string]string{"github/releases": "https://github.com"} }, wantErr: true, }, { name: "generic artifact patterns", modify: func(cfg *Config) { cfg.Upstream.Generic = map[string]string{"apache": "https://archive.apache.org"} cfg.Upstream.GenericArtifacts = map[string][]string{ "apache": {`dist/(?Pmaven)/maven-3/(?P[^/]+)/binaries/(?P[^/]+)`}, } }, }, { name: "generic artifact patterns for an unknown upstream", modify: func(cfg *Config) { cfg.Upstream.GenericArtifacts = map[string][]string{ "apache": {`(?P[^/]+)/(?P[^/]+)`}, } }, wantErr: true, }, { name: "generic artifact pattern does not compile", modify: func(cfg *Config) { cfg.Upstream.Generic = map[string]string{"apache": "https://archive.apache.org"} cfg.Upstream.GenericArtifacts = map[string][]string{"apache": {`(?P[^/]+/(?P[^/]+)`}} }, wantErr: true, }, { name: "generic artifact pattern without a file group", modify: func(cfg *Config) { cfg.Upstream.Generic = map[string]string{"apache": "https://archive.apache.org"} cfg.Upstream.GenericArtifacts = map[string][]string{"apache": {`(?P[^/]+)/[^/]+`}} }, wantErr: true, }, { name: "generic upstream URL is not absolute", modify: func(cfg *Config) { cfg.Upstream.Generic = map[string]string{"github": "github.com"} }, wantErr: true, }, { name: "Helm upstream name contains path separator", modify: func(cfg *Config) { cfg.Upstream.Helm = map[string]string{"team/charts": "https://charts.example.com"} }, wantErr: true, }, { name: "OCI mirrors for a configured upstream", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"ghcr": "https://artifactory.example/api/docker/ghcr-remote"} cfg.Upstream.OCIMirrors = map[string][]string{"ghcr": {"ghcr.io", "registry.example:5000"}} }, }, { name: "OCI mirrors for an unknown upstream", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"ghcr": "https://ghcr.io"} cfg.Upstream.OCIMirrors = map[string][]string{"quay": {"quay.io"}} }, wantErr: true, }, { name: "OCI mirror given as URL", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"ghcr": "https://mirror.example"} cfg.Upstream.OCIMirrors = map[string][]string{"ghcr": {"https://ghcr.io"}} }, wantErr: true, }, { name: "OCI mirror with a path", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"ghcr": "https://mirror.example"} cfg.Upstream.OCIMirrors = map[string][]string{"ghcr": {"ghcr.io/owner"}} }, wantErr: true, }, { name: "OCI mirror host with credentials", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"ghcr": "https://mirror.example"} cfg.Upstream.OCIMirrors = map[string][]string{"ghcr": {"user@ghcr.io"}} }, wantErr: true, }, { name: "OCI mirror host listed for two upstreams", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"art": "https://art.example", "nexus": "https://nexus.example"} cfg.Upstream.OCIMirrors = map[string][]string{"art": {"ghcr.io"}, "nexus": {"GHCR.io:443"}} }, wantErr: true, }, { name: "OCI mirror host listed for two upstreams with an empty port", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"art": "https://art.example", "nexus": "https://nexus.example"} cfg.Upstream.OCIMirrors = map[string][]string{"art": {"ghcr.io"}, "nexus": {"ghcr.io:"}} }, wantErr: true, }, { name: "OCI mirror IPv6 host listed for two upstreams", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"art": "https://art.example", "nexus": "https://nexus.example"} cfg.Upstream.OCIMirrors = map[string][]string{"art": {"[::1]"}, "nexus": {"[::1]:"}} }, wantErr: true, }, { name: "OCI mirror hosts that differ only in a non-default port", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"art": "https://art.example", "nexus": "https://nexus.example"} cfg.Upstream.OCIMirrors = map[string][]string{"art": {"registry.example"}, "nexus": {"registry.example:5000"}} }, }, { name: "OCI mirror host is empty", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"ghcr": "https://mirror.example"} cfg.Upstream.OCIMirrors = map[string][]string{"ghcr": {""}} }, wantErr: true, }, { name: "OCI upstream URL is not absolute", modify: func(cfg *Config) { cfg.Upstream.OCI = map[string]string{"private": "registry.example.com"} }, wantErr: true, }, { name: "APK upstream name contains path separator", modify: func(cfg *Config) { cfg.Upstream.APK = map[string]string{"alpine/edge": "https://dl-cdn.alpinelinux.org/alpine"} }, wantErr: true, }, { name: "APK upstream URL is not absolute", modify: func(cfg *Config) { cfg.Upstream.APK = map[string]string{"private": "apk.example.com"} }, wantErr: true, }, { name: "Arch upstream name contains path separator", modify: func(cfg *Config) { cfg.Upstream.Arch = map[string]string{"arch/linux": "https://geo.mirror.pkgbuild.com"} }, wantErr: true, }, { name: "Arch upstream URL is not absolute", modify: func(cfg *Config) { cfg.Upstream.Arch = map[string]string{"archlinux": "geo.mirror.pkgbuild.com"} }, wantErr: true, }, { name: "valid Debian repository", modify: func(cfg *Config) { cfg.Upstream.DebianRepositories = map[string]string{ "security": "https://security.debian.org/debian-security", } }, }, { name: "Debian repository name contains path separator", modify: func(cfg *Config) { cfg.Upstream.DebianRepositories = map[string]string{ "debian/security": "https://security.debian.org/debian-security", } }, wantErr: true, }, { name: "Debian repository name is a relative path element", modify: func(cfg *Config) { cfg.Upstream.DebianRepositories = map[string]string{ ".": "https://security.debian.org/debian-security", } }, wantErr: true, }, { name: "Debian repository URL is not absolute", modify: func(cfg *Config) { cfg.Upstream.DebianRepositories = map[string]string{"security": "security.debian.org"} }, wantErr: true, }, { name: "Debian repository shadows the pool prefix", modify: func(cfg *Config) { cfg.Upstream.DebianRepositories = map[string]string{"pool": "https://security.debian.org"} }, wantErr: true, }, { name: "Debian repository shadows the dists prefix", modify: func(cfg *Config) { cfg.Upstream.DebianRepositories = map[string]string{"dists": "https://security.debian.org"} }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.modify(cfg) err := cfg.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %t", err, tt.wantErr) } }) } } func TestValidateGoSumDBs(t *testing.T) { tests := []struct { name string modify func(*Config) wantErr bool }{ { name: "valid Go checksum database", modify: func(cfg *Config) { cfg.Upstream.GoSumDBs = map[string]string{"sum.golang.org": "https://sum.golang.org"} }, }, { name: "Go checksum database name contains path separator", modify: func(cfg *Config) { cfg.Upstream.GoSumDBs = map[string]string{"sum.golang.org/x": "https://sum.golang.org"} }, wantErr: true, }, { name: "Go checksum database URL is not absolute", modify: func(cfg *Config) { cfg.Upstream.GoSumDBs = map[string]string{"sum.golang.org": "sum.golang.org"} }, wantErr: true, }, { name: "Go checksum database offline mode requires metadata caching", modify: func(cfg *Config) { cfg.CacheMetadata = false cfg.Upstream.GoSumDBsOffline = true }, wantErr: true, }, { name: "Go checksum database offline mode with metadata caching", modify: func(cfg *Config) { cfg.CacheMetadata = true cfg.Upstream.GoSumDBsOffline = true }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.modify(cfg) err := cfg.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %t", err, tt.wantErr) } }) } } func TestValidateCacheArtifactsDisabled(t *testing.T) { tests := []struct { name string modify func(*Config) wantErr string }{ {"alone", func(*Config) {}, ""}, {"with scanning", func(c *Config) { c.Scanning.Enabled = true }, "scanning.enabled"}, {"with direct_serve", func(c *Config) { c.Storage.DirectServe = true }, "storage.direct_serve"}, {"with mirror_api", func(c *Config) { c.MirrorAPI = true }, "mirror_api"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.Storage.CacheArtifacts = false tt.modify(cfg) err := cfg.Validate() if tt.wantErr == "" { if err != nil { t.Fatalf("unexpected error: %v", err) } return } if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("Validate() = %v, want an error mentioning %q", err, tt.wantErr) } }) } } func TestCacheArtifactsDefaultsToTrue(t *testing.T) { if !Default().Storage.CacheArtifacts { t.Fatal("Default().Storage.CacheArtifacts = false, want true") } path := filepath.Join(t.TempDir(), "config.yaml") if err := os.WriteFile(path, []byte("storage:\n url: \"file:///tmp/cache\"\n"), 0o600); err != nil { t.Fatal(err) } cfg, err := Load(path) if err != nil { t.Fatalf("Load: %v", err) } if !cfg.Storage.CacheArtifacts { t.Error("a config file without storage.cache_artifacts disabled artifact caching") } } func TestLoadCacheArtifactsFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_STORAGE_CACHE_ARTIFACTS", "false") cfg.LoadFromEnv() if cfg.Storage.CacheArtifacts { t.Error("Storage.CacheArtifacts should be false") } } func TestParseArtifactMaxSize(t *testing.T) { tests := []struct { name string size string want int64 }{ {"unset is unlimited", "", 0}, {"zero is unlimited", "0", 0}, {"explicit value", "2GB", 2 << 30}, {"bytes", "1024", 1024}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() cfg.ArtifactMaxSize = tt.size if got := cfg.ParseArtifactMaxSize(); got != tt.want { t.Errorf("ParseArtifactMaxSize() = %d, want %d", got, tt.want) } }) } } func TestValidateArtifactMaxSize(t *testing.T) { for _, invalid := range []string{"not-a-size", "-1"} { cfg := Default() cfg.ArtifactMaxSize = invalid if err := cfg.Validate(); err == nil { t.Errorf("expected validation error for artifact_max_size %q", invalid) } } for _, valid := range []string{"", "0", "2GB"} { cfg := Default() cfg.ArtifactMaxSize = valid if err := cfg.Validate(); err != nil { t.Errorf("unexpected error for artifact_max_size %q: %v", valid, err) } } } func TestLoadArtifactMaxSizeFromEnv(t *testing.T) { cfg := Default() t.Setenv("PROXY_ARTIFACT_MAX_SIZE", "2GB") cfg.LoadFromEnv() if cfg.ArtifactMaxSize != "2GB" { t.Errorf("ArtifactMaxSize = %q, want %q", cfg.ArtifactMaxSize, "2GB") } }