package config import ( "strings" "testing" "time" "github.com/git-pkgs/proxy/internal/retention" ) const retentionDay = 24 * time.Hour func retentionTestRegistry() *retention.Registry { reg := retention.NewRegistry() reg.Register(retention.Spec{Key: "generic", CanonicalPackage: retention.DefaultCanonical("generic")}) reg.Register(retention.Spec{Key: "npm", CanonicalPackage: retention.DefaultCanonical("npm")}) reg.Register(retention.Spec{Key: "julia"}) reg.Register(retention.Spec{Key: "helm"}) return reg } func TestRetentionRulesUnsetIsDisabled(t *testing.T) { rules, err := Default().RetentionRules(retentionTestRegistry()) if err != nil { t.Fatalf("RetentionRules() error = %v", err) } if got := rules.MinPositive(); got != 0 { t.Errorf("MinPositive() = %v, want 0 for an unset retention block", got) } } func TestRetentionRulesResolves(t *testing.T) { cfg := Default() cfg.Storage.Retention = RetentionConfig{ Default: "30d", Ecosystems: map[string]string{"npm": "14d"}, Packages: map[string]string{ "pkg:npm/lodash": "0", "pkg:npm/@babel/core": "90d", "pkg:generic/jqlang%2Fjq": "7d", }, SweepInterval: "5m", } rules, err := cfg.RetentionRules(retentionTestRegistry()) if err != nil { t.Fatalf("RetentionRules() error = %v", err) } if rules.Default != 30*retentionDay { t.Errorf("Default = %v, want 30d", rules.Default) } if got := rules.For("npm", "pkg:npm/left-pad"); got != 14*retentionDay { t.Errorf("npm rule = %v, want 14d", got) } if got, ok := rules.Packages["pkg:npm/lodash"]; !ok || got != 0 { t.Errorf("lodash rule = %v, %v, want 0, true", got, ok) } if got := rules.For("npm", "pkg:npm/%40babel/core"); got != 90*retentionDay { t.Errorf("babel rule = %v, want 90d under its canonical key", got) } if got := rules.For("generic", "pkg:generic/jqlang%2Fjq"); got != 7*retentionDay { t.Errorf("generic package rule = %v, want 7d", got) } if got := rules.For("generic", "pkg:generic/other%2Frepo"); got != 0 { t.Errorf("unconfigured generic package rule = %v, want 0 despite the global default", got) } if got := cfg.ParseRetentionSweepInterval(); got != 5*time.Minute { t.Errorf("ParseRetentionSweepInterval() = %v, want 5m", got) } } func TestRetentionRulesErrors(t *testing.T) { tests := []struct { name string modify func(*RetentionConfig) wantErr string }{ {"bad default", func(r *RetentionConfig) { r.Default = "7x" }, "storage.retention.default"}, {"negative default", func(r *RetentionConfig) { r.Default = "-1d" }, "must not be negative"}, {"NaN days", func(r *RetentionConfig) { r.Default = "NaNd" }, "out of range"}, {"infinite days", func(r *RetentionConfig) { r.Default = "Infd" }, "out of range"}, {"overflowing days", func(r *RetentionConfig) { r.Default = "1e9d" }, "out of range"}, {"zero sweep interval", func(r *RetentionConfig) { r.SweepInterval = "0s" }, "must be at least 1m0s"}, {"sweep interval below a minute", func(r *RetentionConfig) { r.SweepInterval = "30s" }, "must be at least 1m0s"}, {"bad sweep interval", func(r *RetentionConfig) { r.SweepInterval = "often" }, "sweep_interval"}, {"unknown ecosystem", func(r *RetentionConfig) { r.Ecosystems = map[string]string{"nmp": "1d"} }, "unknown ecosystem"}, {"ecosystem not registered", func(r *RetentionConfig) { r.Ecosystems = map[string]string{"oci": "14d"} }, `storage.retention.ecosystems.oci: retention is not supported for "oci"`}, {"bad ecosystem duration", func(r *RetentionConfig) { r.Ecosystems = map[string]string{"npm": "soon"} }, "storage.retention.ecosystems.npm"}, {"package of unregistered ecosystem", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:cargo/serde": "1d"} }, "not supported for cargo packages"}, {"julia package", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:julia/Example": "1d"} }, "not supported for julia"}, {"package of ecosystem without package rules", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:helm/nginx": "1d"} }, "package rules are not supported for helm; use storage.retention.ecosystems.helm"}, {"invalid package PURL", func(r *RetentionConfig) { r.Packages = map[string]string{"lodash": "1d"} }, "not a valid package PURL"}, {"npm package without its scope marker", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:npm/babel/core": "1d"} }, "does not match how npm packages are stored"}, {"npm package with an encoded scope separator", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:npm/%40babel%2Fcore": "1d"} }, "does not match how npm packages are stored"}, {"versioned package PURL", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:npm/lodash@4.17.21": "1d"} }, "must not carry a version"}, {"conflicting package keys", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:npm/%40babel/core": "1d", "pkg:npm/@babel/core": "2d"} }, "conflicts with another key"}, {"package PURL with qualifiers", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:npm/lodash?type=tgz": "1d"} }, "must not carry qualifiers or a subpath"}, {"package PURL with subpath", func(r *RetentionConfig) { r.Packages = map[string]string{"pkg:npm/lodash#lib": "1d"} }, "must not carry qualifiers or a subpath"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := Default() tt.modify(&cfg.Storage.Retention) _, err := cfg.RetentionRules(retentionTestRegistry()) if err == nil { t.Fatalf("RetentionRules() succeeded, want error containing %q", tt.wantErr) } if !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("RetentionRules() error = %q, want it to contain %q", err, tt.wantErr) } }) } } // Validate checks retention against the handlers' registry, which this // package's tests leave empty, so any ecosystem rule fails while a bare // default passes. func TestValidateRetention(t *testing.T) { cfg := Default() cfg.Storage.Retention.Default = "30d" if err := cfg.Validate(); err != nil { t.Errorf("Validate() with only a default = %v, want nil", err) } cfg.Storage.Retention.Ecosystems = map[string]string{"npm": "14d"} err := cfg.Validate() if err == nil || !strings.Contains(err.Error(), "not supported") { t.Errorf("Validate() with an unsupported ecosystem = %v, want a not supported error", err) } } func TestRetentionEnvOverrides(t *testing.T) { t.Setenv("PROXY_STORAGE_RETENTION_DEFAULT", "21d") t.Setenv("PROXY_STORAGE_RETENTION_SWEEP_INTERVAL", "1h") cfg := Default() cfg.LoadFromEnv() if cfg.Storage.Retention.Default != "21d" { t.Errorf("Retention.Default = %q, want 21d", cfg.Storage.Retention.Default) } if got := cfg.ParseRetentionSweepInterval(); got != time.Hour { t.Errorf("ParseRetentionSweepInterval() = %v, want 1h", got) } } func TestRetentionSweepIntervalDefault(t *testing.T) { cfg := Default() if got := cfg.ParseRetentionSweepInterval(); got != 10*time.Minute { t.Errorf("ParseRetentionSweepInterval() = %v, want 10m", got) } } func TestGradleBuildCacheMaxAgeAcceptsDays(t *testing.T) { cfg := Default() cfg.Gradle.BuildCache.MaxAge = "7d" if err := cfg.Validate(); err != nil { t.Fatalf("Validate() with max_age 7d = %v, want nil", err) } if got := cfg.ParseGradleBuildCacheMaxAge(); got != 7*retentionDay { t.Errorf("ParseGradleBuildCacheMaxAge() = %v, want 168h", got) } }