package config_test import ( "testing" "time" . "github.com/smartystreets/goconvey/convey" "zotregistry.dev/zot/v2/pkg/api/config" "zotregistry.dev/zot/v2/pkg/compat" extconf "zotregistry.dev/zot/v2/pkg/extensions/config" eventsconf "zotregistry.dev/zot/v2/pkg/extensions/config/events" syncconf "zotregistry.dev/zot/v2/pkg/extensions/config/sync" ) func TestConfig(t *testing.T) { Convey("Test config utils", t, func() { firstStorageConfig := config.StorageConfig{ GC: true, Dedupe: true, GCDelay: 1 * time.Minute, GCInterval: 1 * time.Hour, } secondStorageConfig := config.StorageConfig{ GC: true, Dedupe: true, GCDelay: 1 * time.Minute, GCInterval: 1 * time.Hour, } So(firstStorageConfig.ParamsEqual(secondStorageConfig), ShouldBeTrue) firstStorageConfig.GC = false So(firstStorageConfig.ParamsEqual(secondStorageConfig), ShouldBeFalse) firstStorageConfig.GC = true firstStorageConfig.Dedupe = false So(firstStorageConfig.ParamsEqual(secondStorageConfig), ShouldBeFalse) firstStorageConfig.Dedupe = true firstStorageConfig.GCDelay = 2 * time.Minute So(firstStorageConfig.ParamsEqual(secondStorageConfig), ShouldBeFalse) firstStorageConfig.GCDelay = 1 * time.Minute firstStorageConfig.GCInterval = 2 * time.Hour So(firstStorageConfig.ParamsEqual(secondStorageConfig), ShouldBeFalse) firstStorageConfig.GCInterval = 1 * time.Hour So(firstStorageConfig.ParamsEqual(secondStorageConfig), ShouldBeTrue) isSame, err := config.SameFile("test-config", "test") So(err, ShouldNotBeNil) So(isSame, ShouldBeFalse) dir1 := t.TempDir() isSame, err = config.SameFile(dir1, "test") So(err, ShouldNotBeNil) So(isSame, ShouldBeFalse) dir2 := t.TempDir() isSame, err = config.SameFile(dir1, dir2) So(err, ShouldBeNil) So(isSame, ShouldBeFalse) isSame, err = config.SameFile(dir1, dir1) So(err, ShouldBeNil) So(isSame, ShouldBeTrue) }) Convey("Test DeepCopy() & Sanitize()", t, func() { Convey("Test DeepCopy negative cases", func() { conf := config.New() So(conf, ShouldNotBeNil) // negative obj := make(chan int) err := config.DeepCopy(conf, obj) So(err, ShouldNotBeNil) err = config.DeepCopy(obj, conf) So(err, ShouldNotBeNil) }) Convey("Test Sanitize() with LDAP bind password", func() { conf := config.New() So(conf, ShouldNotBeNil) // Set LDAP bind password authConfig := &config.AuthConfig{LDAP: (&config.LDAPConfig{}).SetBindPassword("secret-ldap-password")} conf.HTTP.Auth = authConfig So(func() { conf.Sanitize() }, ShouldNotPanic) sanitizedConf := conf.Sanitize() So(sanitizedConf.HTTP.Auth.LDAP.BindPassword(), ShouldEqual, "******") // Verify original config is not modified So(conf.HTTP.Auth.LDAP.BindPassword(), ShouldEqual, "secret-ldap-password") }) Convey("Test Sanitize() with OpenID client secrets", func() { conf := config.New() So(conf, ShouldNotBeNil) // Set OpenID client secrets authConfig := &config.AuthConfig{ OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "google": { Name: "Google", ClientID: "google-client-id", ClientSecret: "google-client-secret", Issuer: "https://accounts.google.com", Scopes: []string{"openid", "email"}, }, "github": { Name: "GitHub", ClientID: "github-client-id", ClientSecret: "github-client-secret", AuthURL: "github-auth-url", TokenURL: "github-token-url", Scopes: []string{"user:email"}, }, }, }, } conf.HTTP.Auth = authConfig So(func() { conf.Sanitize() }, ShouldNotPanic) sanitizedConf := conf.Sanitize() // Verify OpenID client secrets are sanitized So(sanitizedConf.HTTP.Auth.OpenID.Providers["google"].ClientSecret, ShouldEqual, "******") So(sanitizedConf.HTTP.Auth.OpenID.Providers["github"].ClientSecret, ShouldEqual, "******") // Verify other fields are preserved So(sanitizedConf.HTTP.Auth.OpenID.Providers["google"].ClientID, ShouldEqual, "google-client-id") So(sanitizedConf.HTTP.Auth.OpenID.Providers["google"].Name, ShouldEqual, "Google") So(sanitizedConf.HTTP.Auth.OpenID.Providers["google"].Issuer, ShouldEqual, "https://accounts.google.com") So(sanitizedConf.HTTP.Auth.OpenID.Providers["google"].Scopes, ShouldResemble, []string{"openid", "email"}) // Verify original config is not modified So(conf.HTTP.Auth.OpenID.Providers["google"].ClientSecret, ShouldEqual, "google-client-secret") So(conf.HTTP.Auth.OpenID.Providers["github"].ClientSecret, ShouldEqual, "github-client-secret") So(conf.HTTP.Auth.OpenID.Providers["github"].AuthURL, ShouldEqual, "github-auth-url") So(conf.HTTP.Auth.OpenID.Providers["github"].TokenURL, ShouldEqual, "github-token-url") }) Convey("Test Sanitize() with Event sink credentials", func() { conf := config.New() So(conf, ShouldNotBeNil) // Enable events extension and set sink credentials enabled := true conf.Extensions = &extconf.ExtensionConfig{ Events: &eventsconf.Config{ Enable: &enabled, Sinks: []eventsconf.SinkConfig{ { Type: eventsconf.HTTP, Address: "https://example.com/webhook", Credentials: &eventsconf.Credentials{ Username: "webhook-user", Password: "webhook-password", }, }, { Type: eventsconf.NATS, Address: "nats://localhost:4222", Credentials: &eventsconf.Credentials{ Username: "nats-user", Password: "nats-token", }, }, }, }, } So(func() { conf.Sanitize() }, ShouldNotPanic) sanitizedConf := conf.Sanitize() // Verify event sink credentials passwords are sanitized So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "******") So(sanitizedConf.Extensions.Events.Sinks[1].Credentials.Password, ShouldEqual, "******") // Verify other fields are preserved So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Username, ShouldEqual, "webhook-user") So(sanitizedConf.Extensions.Events.Sinks[1].Credentials.Username, ShouldEqual, "nats-user") So(sanitizedConf.Extensions.Events.Sinks[0].Type, ShouldEqual, eventsconf.HTTP) So(sanitizedConf.Extensions.Events.Sinks[1].Type, ShouldEqual, eventsconf.NATS) // Verify original config is not modified So(conf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "webhook-password") So(conf.Extensions.Events.Sinks[1].Credentials.Password, ShouldEqual, "nats-token") }) Convey("Test Sanitize() with Event sink credentials including nil credentials", func() { conf := config.New() So(conf, ShouldNotBeNil) // Enable events extension with mixed sink credentials (some nil, some not) enabled := true conf.Extensions = &extconf.ExtensionConfig{ Events: &eventsconf.Config{ Enable: &enabled, Sinks: []eventsconf.SinkConfig{ { Type: eventsconf.HTTP, Address: "https://example.com/webhook", Credentials: &eventsconf.Credentials{ Username: "webhook-user", Password: "webhook-password", }, }, { Type: eventsconf.NATS, Address: "nats://localhost:4222", Credentials: nil, // This should trigger the continue statement }, { Type: eventsconf.HTTP, Address: "https://another.com/webhook", Credentials: &eventsconf.Credentials{ Username: "another-user", Password: "another-password", }, }, }, }, } So(func() { conf.Sanitize() }, ShouldNotPanic) sanitizedConf := conf.Sanitize() // Verify that sinks with credentials have their passwords sanitized So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "******") So(sanitizedConf.Extensions.Events.Sinks[2].Credentials.Password, ShouldEqual, "******") // Verify that sink with nil credentials is preserved as-is (no panic, no modification) So(sanitizedConf.Extensions.Events.Sinks[1].Credentials, ShouldBeNil) So(sanitizedConf.Extensions.Events.Sinks[1].Type, ShouldEqual, eventsconf.NATS) So(sanitizedConf.Extensions.Events.Sinks[1].Address, ShouldEqual, "nats://localhost:4222") // Verify other fields are preserved So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Username, ShouldEqual, "webhook-user") So(sanitizedConf.Extensions.Events.Sinks[2].Credentials.Username, ShouldEqual, "another-user") So(sanitizedConf.Extensions.Events.Sinks[0].Type, ShouldEqual, eventsconf.HTTP) So(sanitizedConf.Extensions.Events.Sinks[2].Type, ShouldEqual, eventsconf.HTTP) // Verify original config is not modified So(conf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "webhook-password") So(conf.Extensions.Events.Sinks[2].Credentials.Password, ShouldEqual, "another-password") So(conf.Extensions.Events.Sinks[1].Credentials, ShouldBeNil) }) Convey("Test Sanitize() with all sensitive data types", func() { conf := config.New() So(conf, ShouldNotBeNil) // Set all types of sensitive data authConfig := &config.AuthConfig{ LDAP: (&config.LDAPConfig{}).SetBindPassword("ldap-secret"), OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "azure": { Name: "Azure AD", ClientID: "azure-client-id", ClientSecret: "azure-client-secret", Issuer: "https://login.microsoftonline.com/...", }, }, }, } conf.HTTP.Auth = authConfig // Enable events extension enabled := true conf.Extensions = &extconf.ExtensionConfig{ Events: &eventsconf.Config{ Enable: &enabled, Sinks: []eventsconf.SinkConfig{ { Type: eventsconf.HTTP, Address: "https://smtp.example.com/webhook", Credentials: &eventsconf.Credentials{ Username: "smtp-user", Password: "smtp-password", }, }, }, }, } So(func() { conf.Sanitize() }, ShouldNotPanic) sanitizedConf := conf.Sanitize() // Verify all sensitive data is sanitized So(sanitizedConf.HTTP.Auth.LDAP.BindPassword(), ShouldEqual, "******") So(sanitizedConf.HTTP.Auth.OpenID.Providers["azure"].ClientSecret, ShouldEqual, "******") So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "******") // Verify non-sensitive data is preserved So(sanitizedConf.HTTP.Auth.OpenID.Providers["azure"].ClientID, ShouldEqual, "azure-client-id") So(sanitizedConf.HTTP.Auth.OpenID.Providers["azure"].Name, ShouldEqual, "Azure AD") So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Username, ShouldEqual, "smtp-user") So(sanitizedConf.Extensions.Events.Sinks[0].Type, ShouldEqual, eventsconf.HTTP) }) Convey("Test Sanitize() with nil sensitive data", func() { conf := config.New() So(conf, ShouldNotBeNil) // Set config with nil sensitive data authConfig := &config.AuthConfig{ LDAP: nil, // No LDAP config OpenID: nil, // No OpenID config } conf.HTTP.Auth = authConfig // No events extension conf.Extensions = nil So(func() { conf.Sanitize() }, ShouldNotPanic) sanitizedConf := conf.Sanitize() // Verify nil configs are handled gracefully So(sanitizedConf.HTTP.Auth.LDAP, ShouldBeNil) So(sanitizedConf.HTTP.Auth.OpenID, ShouldBeNil) So(sanitizedConf.Extensions, ShouldBeNil) }) Convey("Test Sanitize() with empty sensitive data", func() { conf := config.New() So(conf, ShouldNotBeNil) // Set config with empty sensitive data authConfig := &config.AuthConfig{ LDAP: (&config.LDAPConfig{}).SetBindPassword(""), // Empty password OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "empty": { Name: "Empty Provider", ClientID: "empty-client-id", ClientSecret: "", // Empty secret }, }, }, } conf.HTTP.Auth = authConfig // Enable events extension with empty password enabled := true conf.Extensions = &extconf.ExtensionConfig{ Events: &eventsconf.Config{ Enable: &enabled, Sinks: []eventsconf.SinkConfig{ { Type: eventsconf.HTTP, Address: "https://example.com/webhook", Credentials: &eventsconf.Credentials{ Username: "user", Password: "", // Empty password }, }, }, }, } So(func() { conf.Sanitize() }, ShouldNotPanic) sanitizedConf := conf.Sanitize() // Verify empty passwords behavior // LDAP empty password should remain empty So(sanitizedConf.HTTP.Auth.LDAP.BindPassword(), ShouldEqual, "") // OpenID empty secret is always sanitized So(sanitizedConf.HTTP.Auth.OpenID.Providers["empty"].ClientSecret, ShouldEqual, "******") // Event sink empty password is always sanitized So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "******") }) Convey("Test Sanitize() with nil config", func() { var conf *config.Config = nil So(func() { conf.Sanitize() }, ShouldNotPanic) sanitizedConf := conf.Sanitize() So(sanitizedConf, ShouldBeNil) }) }) Convey("Test IsRetentionEnabled()", t, func() { // Test nil config var nilConf *config.Config = nil So(nilConf.IsRetentionEnabled(), ShouldBeFalse) conf := config.New() So(conf.IsRetentionEnabled(), ShouldBeFalse) conf.Storage.Retention.Policies = []config.RetentionPolicy{ { Repositories: []string{"repo"}, }, } So(conf.IsRetentionEnabled(), ShouldBeFalse) policies := []config.RetentionPolicy{ { Repositories: []string{"repo"}, KeepTags: []config.KeepTagsPolicy{ { Patterns: []string{"tag"}, MostRecentlyPulledCount: 2, }, }, }, } conf.Storage.Retention = config.ImageRetention{ Policies: policies, } So(conf.IsRetentionEnabled(), ShouldBeTrue) subPaths := make(map[string]config.StorageConfig) subPaths["/a"] = config.StorageConfig{ GC: true, Retention: config.ImageRetention{ Policies: policies, }, } conf.Storage.SubPaths = subPaths So(conf.IsRetentionEnabled(), ShouldBeTrue) // Test MostRecentlyPushedCount conf = config.New() conf.Storage.Retention.Policies = []config.RetentionPolicy{ { Repositories: []string{"repo"}, KeepTags: []config.KeepTagsPolicy{ { Patterns: []string{"tag"}, MostRecentlyPushedCount: 3, }, }, }, } So(conf.IsRetentionEnabled(), ShouldBeTrue) // Test PulledWithin conf = config.New() duration := time.Hour * 24 conf.Storage.Retention.Policies = []config.RetentionPolicy{ { Repositories: []string{"repo"}, KeepTags: []config.KeepTagsPolicy{ { Patterns: []string{"tag"}, PulledWithin: &duration, }, }, }, } So(conf.IsRetentionEnabled(), ShouldBeTrue) // Test PushedWithin conf = config.New() conf.Storage.Retention.Policies = []config.RetentionPolicy{ { Repositories: []string{"repo"}, KeepTags: []config.KeepTagsPolicy{ { Patterns: []string{"tag"}, PushedWithin: &duration, }, }, }, } So(conf.IsRetentionEnabled(), ShouldBeTrue) // Test SubPaths with retention policies conf = config.New() conf.Storage.SubPaths = map[string]config.StorageConfig{ "subpath1": { Retention: config.ImageRetention{ Policies: []config.RetentionPolicy{ { Repositories: []string{"repo1"}, KeepTags: []config.KeepTagsPolicy{ { Patterns: []string{"latest"}, MostRecentlyPulledCount: 5, }, }, }, }, }, }, } So(conf.IsRetentionEnabled(), ShouldBeTrue) // Test empty policies with no retention criteria conf = config.New() conf.Storage.Retention.Policies = []config.RetentionPolicy{ { Repositories: []string{"repo"}, KeepTags: []config.KeepTagsPolicy{ { Patterns: []string{"tag"}, // No retention criteria set }, }, }, } So(conf.IsRetentionEnabled(), ShouldBeFalse) }) Convey("Test IsEventRecorderEnabled()", t, func() { conf := config.New() extensionsConfig := conf.CopyExtensionsConfig() So(extensionsConfig.IsEventRecorderEnabled(), ShouldBeFalse) // Enable the event recorder enable := true conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Events = &eventsconf.Config{ Enable: &enable, } extensionsConfig = conf.CopyExtensionsConfig() So(extensionsConfig.IsEventRecorderEnabled(), ShouldBeTrue) // Disabled scenario disable := false conf.Extensions.Events.Enable = &disable extensionsConfig = conf.CopyExtensionsConfig() So(extensionsConfig.IsEventRecorderEnabled(), ShouldBeFalse) // nil pointers conf.Extensions.Events = nil extensionsConfig = conf.CopyExtensionsConfig() So(extensionsConfig.IsEventRecorderEnabled(), ShouldBeFalse) conf.Extensions = nil extensionsConfig = conf.CopyExtensionsConfig() So(extensionsConfig.IsEventRecorderEnabled(), ShouldBeFalse) }) Convey("Test AccessControlConfig.ContainsOnlyAnonymousPolicy()", t, func() { Convey("When accessControlConfig is nil", func() { var accessControlConfig *config.AccessControlConfig = nil result := accessControlConfig.ContainsOnlyAnonymousPolicy() So(result, ShouldBeTrue) }) Convey("When accessControlConfig has admin policies", func() { accessControlConfig := &config.AccessControlConfig{} accessControlConfig.AdminPolicy = config.Policy{ Actions: []string{"read"}, Users: []string{"admin"}, } result := accessControlConfig.ContainsOnlyAnonymousPolicy() So(result, ShouldBeFalse) }) Convey("When accessControlConfig has only anonymous policies", func() { accessControlConfig := &config.AccessControlConfig{} accessControlConfig.Repositories = config.Repositories{ "repo1": config.PolicyGroup{ AnonymousPolicy: []string{"read"}, }, } result := accessControlConfig.ContainsOnlyAnonymousPolicy() So(result, ShouldBeTrue) }) Convey("When accessControlConfig has default policies", func() { accessControlConfig := &config.AccessControlConfig{} accessControlConfig.Repositories = config.Repositories{ "repo1": config.PolicyGroup{ DefaultPolicy: []string{"read"}, }, } result := accessControlConfig.ContainsOnlyAnonymousPolicy() So(result, ShouldBeFalse) }) Convey("When accessControlConfig has non-empty repository policies", func() { accessControlConfig := &config.AccessControlConfig{} accessControlConfig.Repositories = config.Repositories{ "repo1": config.PolicyGroup{ Policies: []config.Policy{ { Actions: []string{"read"}, Users: []string{"user1"}, }, }, }, } result := accessControlConfig.ContainsOnlyAnonymousPolicy() So(result, ShouldBeFalse) }) Convey("When accessControlConfig has empty admin policy and no repositories", func() { accessControlConfig := &config.AccessControlConfig{} accessControlConfig.AdminPolicy = config.Policy{ Actions: []string{}, Users: []string{}, } accessControlConfig.Repositories = config.Repositories{} result := accessControlConfig.ContainsOnlyAnonymousPolicy() So(result, ShouldBeFalse) }) Convey("When accessControlConfig has empty policies in repository", func() { accessControlConfig := &config.AccessControlConfig{} accessControlConfig.Repositories = config.Repositories{ "repo1": config.PolicyGroup{ AnonymousPolicy: []string{"read"}, Policies: []config.Policy{ { Actions: []string{}, Users: []string{}, }, }, }, } result := accessControlConfig.ContainsOnlyAnonymousPolicy() So(result, ShouldBeTrue) }) }) Convey("Test AuthConfig methods", t, func() { Convey("Test IsLdapAuthEnabled()", func() { // Test with nil AuthConfig var authConfig *config.AuthConfig = nil So(authConfig.IsLdapAuthEnabled(), ShouldBeFalse) // Test with AuthConfig but nil LDAP authConfig = &config.AuthConfig{} So(authConfig.IsLdapAuthEnabled(), ShouldBeFalse) // Test with AuthConfig and LDAP configured authConfig = &config.AuthConfig{ LDAP: &config.LDAPConfig{}, } So(authConfig.IsLdapAuthEnabled(), ShouldBeTrue) }) Convey("Test IsHtpasswdAuthEnabled()", func() { // Test with nil AuthConfig var authConfig *config.AuthConfig = nil So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeFalse) // Test with AuthConfig but empty HTPasswd path authConfig = &config.AuthConfig{ HTPasswd: config.AuthHTPasswd{Path: ""}, } So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeFalse) // Test with AuthConfig and HTPasswd configured authConfig = &config.AuthConfig{ HTPasswd: config.AuthHTPasswd{Path: "/path/to/htpasswd"}, } So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeTrue) }) Convey("Test IsBearerAuthEnabled()", func() { // Test with nil AuthConfig var authConfig *config.AuthConfig = nil So(authConfig.IsBearerAuthEnabled(), ShouldBeFalse) // Test with AuthConfig but nil Bearer authConfig = &config.AuthConfig{} So(authConfig.IsBearerAuthEnabled(), ShouldBeFalse) // Test with AuthConfig and Bearer configured with all required fields authConfig = &config.AuthConfig{ Bearer: &config.BearerConfig{ Cert: "/path/to/cert.pem", Realm: "test-realm", Service: "test-service", }, } So(authConfig.IsBearerAuthEnabled(), ShouldBeTrue) }) Convey("Test IsOpenIDAuthEnabled()", func() { // Test with nil AuthConfig var authConfig *config.AuthConfig = nil So(authConfig.IsOpenIDAuthEnabled(), ShouldBeFalse) // Test with AuthConfig but nil OpenID authConfig = &config.AuthConfig{} So(authConfig.IsOpenIDAuthEnabled(), ShouldBeFalse) // Test with AuthConfig and OpenID configured with providers authConfig = &config.AuthConfig{ OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "google": { ClientID: "client-id", }, }, }, } So(authConfig.IsOpenIDAuthEnabled(), ShouldBeTrue) }) Convey("Test IsAPIKeyEnabled()", func() { // Test with nil AuthConfig var authConfig *config.AuthConfig = nil So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse) // Test with AuthConfig but APIKey disabled authConfig = &config.AuthConfig{ APIKey: false, } So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse) // Test with AuthConfig and APIKey enabled authConfig = &config.AuthConfig{ APIKey: true, } So(authConfig.IsAPIKeyEnabled(), ShouldBeTrue) }) Convey("Test IsBasicAuthnEnabled()", func() { // Test with nil AuthConfig var authConfig *config.AuthConfig = nil So(authConfig.IsBasicAuthnEnabled(), ShouldBeFalse) // Test with AuthConfig but no basic auth methods authConfig = &config.AuthConfig{} So(authConfig.IsBasicAuthnEnabled(), ShouldBeFalse) // Test with HTPasswd enabled authConfig = &config.AuthConfig{ HTPasswd: config.AuthHTPasswd{Path: "/path/to/htpasswd"}, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeTrue) // Test with LDAP enabled authConfig = &config.AuthConfig{ LDAP: &config.LDAPConfig{}, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeTrue) // Test with OpenID enabled (with ClientID) authConfig = &config.AuthConfig{ OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "google": { ClientID: "client-id", Scopes: []string{"openid", "email"}, }, }, }, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeTrue) // Test with OpenID enabled (with Issuer) authConfig = &config.AuthConfig{ OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "google": { ClientID: "", Issuer: "https://accounts.google.com", Scopes: []string{}, }, }, }, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeTrue) // Test with OpenID enabled (with Scopes only) authConfig = &config.AuthConfig{ OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "google": { ClientID: "", Issuer: "", Scopes: []string{"openid", "email"}, }, }, }, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeTrue) // Test with OAuth2 provider (github) authConfig = &config.AuthConfig{ OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "github": { ClientID: "github-client-id", Scopes: []string{"user:email"}, }, }, }, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeTrue) // Test with OpenID but no valid providers (empty config) // Note: AuthConfig.IsOpenIDAuthEnabled() only checks if provider is supported, // not if the configuration is valid, so this returns true authConfig = &config.AuthConfig{ OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "google": { ClientID: "", Issuer: "", Scopes: []string{}, }, }, }, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeTrue) // Test with OpenID but unsupported provider authConfig = &config.AuthConfig{ OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "unsupported": { ClientID: "client-id", Scopes: []string{"scope"}, }, }, }, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeFalse) // Test with APIKey enabled authConfig = &config.AuthConfig{ APIKey: true, } So(authConfig.IsBasicAuthnEnabled(), ShouldBeTrue) }) Convey("Test GetFailDelay()", func() { // Test with nil AuthConfig var authConfig *config.AuthConfig = nil So(authConfig.GetFailDelay(), ShouldEqual, 0) // Test with AuthConfig and custom FailDelay authConfig = &config.AuthConfig{ FailDelay: 5, } So(authConfig.GetFailDelay(), ShouldEqual, 5) }) }) Convey("Test LDAPConfig methods", t, func() { Convey("Test BindDN()", func() { ldapConfig := &config.LDAPConfig{} So(ldapConfig.BindDN(), ShouldEqual, "") ldapConfig.SetBindDN("cn=admin,dc=example,dc=com") So(ldapConfig.BindDN(), ShouldEqual, "cn=admin,dc=example,dc=com") }) Convey("Test BindPassword()", func() { ldapConfig := &config.LDAPConfig{} So(ldapConfig.BindPassword(), ShouldEqual, "") ldapConfig.SetBindPassword("secretpassword") So(ldapConfig.BindPassword(), ShouldEqual, "secretpassword") }) }) Convey("Test AccessControlConfig methods", t, func() { Convey("Test IsAuthzEnabled()", func() { // Test with nil AccessControlConfig var accessControlConfig *config.AccessControlConfig = nil So(accessControlConfig.IsAuthzEnabled(), ShouldBeFalse) // Test with AccessControlConfig accessControlConfig = &config.AccessControlConfig{} So(accessControlConfig.IsAuthzEnabled(), ShouldBeTrue) }) Convey("Test AnonymousPolicyExists()", func() { // Test with nil AccessControlConfig var accessControlConfig *config.AccessControlConfig = nil So(accessControlConfig.AnonymousPolicyExists(), ShouldBeFalse) // Test with AccessControlConfig but no repositories accessControlConfig = &config.AccessControlConfig{} So(accessControlConfig.AnonymousPolicyExists(), ShouldBeFalse) // Test with AccessControlConfig and repository with anonymous policy accessControlConfig = &config.AccessControlConfig{} accessControlConfig.Repositories = config.Repositories{ "repo1": config.PolicyGroup{ AnonymousPolicy: []string{"read"}, }, } So(accessControlConfig.AnonymousPolicyExists(), ShouldBeTrue) // Test with AccessControlConfig and repository without anonymous policy accessControlConfig = &config.AccessControlConfig{} accessControlConfig.Repositories = config.Repositories{ "repo1": config.PolicyGroup{ DefaultPolicy: []string{"read"}, }, } So(accessControlConfig.AnonymousPolicyExists(), ShouldBeFalse) }) Convey("Test GetRepositories()", func() { repositories := config.Repositories{ "repo1": config.PolicyGroup{ AnonymousPolicy: []string{"read"}, }, } accessControlConfig := &config.AccessControlConfig{} accessControlConfig.Repositories = repositories So(accessControlConfig.GetRepositories(), ShouldResemble, repositories) }) Convey("Test GetAdminPolicy()", func() { adminPolicy := config.Policy{ Actions: []string{"read", "write"}, Users: []string{"admin"}, } accessControlConfig := &config.AccessControlConfig{} accessControlConfig.AdminPolicy = adminPolicy So(accessControlConfig.GetAdminPolicy(), ShouldResemble, adminPolicy) }) Convey("Test GetMetrics()", func() { metrics := config.Metrics{ Users: []string{"metrics-user"}, } accessControlConfig := &config.AccessControlConfig{} accessControlConfig.Metrics = metrics So(accessControlConfig.GetMetrics(), ShouldResemble, metrics) }) Convey("Test GetGroups()", func() { groups := config.Groups{ "developers": config.Group{ Users: []string{"dev1", "dev2"}, }, } accessControlConfig := &config.AccessControlConfig{} accessControlConfig.Groups = groups So(accessControlConfig.GetGroups(), ShouldResemble, groups) }) }) Convey("Test Config getter methods", t, func() { Convey("Test CopyAuthConfig()", func() { Convey("Test with non-nil Auth", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ FailDelay: 5, }, }, } authConfig := cfg.CopyAuthConfig() So(authConfig, ShouldNotBeNil) So(authConfig.GetFailDelay(), ShouldEqual, 5) }) Convey("Test with nil Auth", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: nil, }, } authConfig := cfg.CopyAuthConfig() So(authConfig, ShouldBeNil) }) Convey("Test that returned AuthConfig is isolated from config mutations", func() { // Create initial config with AuthConfig containing nested structures cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ FailDelay: 5, HTPasswd: config.AuthHTPasswd{ Path: "/etc/htpasswd", }, LDAP: &config.LDAPConfig{ Address: "ldap.example.com", Port: 389, }, Bearer: &config.BearerConfig{ Realm: "test-realm", Service: "test-service", Cert: "/path/to/cert", }, OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "google": { Name: "Google", ClientID: "google-client-id", Scopes: []string{"openid", "email"}, }, }, }, APIKey: false, SessionKeysFile: "/etc/session-keys", SessionHashKey: []byte("hash-key"), SessionEncryptKey: []byte("encrypt-key"), SessionDriver: map[string]any{ "type": "redis", "host": "localhost", }, }, }, } // Get the AuthConfig reference authConfig := cfg.CopyAuthConfig() So(authConfig, ShouldNotBeNil) So(authConfig.GetFailDelay(), ShouldEqual, 5) So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeTrue) So(authConfig.IsLdapAuthEnabled(), ShouldBeTrue) So(authConfig.IsBearerAuthEnabled(), ShouldBeTrue) So(authConfig.IsOpenIDAuthEnabled(), ShouldBeTrue) So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse) // Test deep copy isolation by modifying nested structures authConfig.LDAP.Address = "modified-ldap.example.com" authConfig.Bearer.Realm = "modified-realm" authConfig.OpenID.Providers["google"].Scopes[0] = "modified-scope" authConfig.SessionHashKey[0] = 'M' authConfig.SessionDriver["type"] = "modified-driver" // Verify original is unchanged So(cfg.HTTP.Auth.LDAP.Address, ShouldEqual, "ldap.example.com") So(cfg.HTTP.Auth.Bearer.Realm, ShouldEqual, "test-realm") So(cfg.HTTP.Auth.OpenID.Providers["google"].Scopes[0], ShouldEqual, "openid") So(cfg.HTTP.Auth.SessionHashKey[0], ShouldEqual, byte('h')) So(cfg.HTTP.Auth.SessionDriver["type"], ShouldEqual, "redis") }) Convey("Test that returned AuthConfig is isolated when config is updated via UpdateReloadableConfig", func() { // Create initial config with AuthConfig cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ FailDelay: 5, HTPasswd: config.AuthHTPasswd{ Path: "/etc/htpasswd", }, APIKey: false, }, }, } // Get the AuthConfig reference authConfig := cfg.CopyAuthConfig() So(authConfig, ShouldNotBeNil) So(authConfig.GetFailDelay(), ShouldEqual, 5) So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeTrue) So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse) // Create new config with updated AuthConfig // Note: UpdateReloadableConfig updates HTPasswd, LDAP, APIKey, and OpenID fields newConfig := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ FailDelay: 15, // This field is NOT updated by UpdateReloadableConfig HTPasswd: config.AuthHTPasswd{ Path: "/etc/updated-htpasswd", // This field IS updated by UpdateReloadableConfig }, APIKey: true, // This field IS updated by UpdateReloadableConfig }, }, } // Update the config using UpdateReloadableConfig cfg.UpdateReloadableConfig(newConfig) // Verify that the returned AuthConfig is not affected by the update // CopyAuthConfig() returns a copy, so the returned object should be isolated So(authConfig.GetFailDelay(), ShouldEqual, 5) // Should remain unchanged So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeTrue) // Should remain unchanged (old path) So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse) // Should remain unchanged // Verify that a new CopyAuthConfig() call returns the updated values newAuthConfig := cfg.CopyAuthConfig() So(newAuthConfig, ShouldNotBeNil) // Should remain unchanged (not updated by UpdateReloadableConfig) So(newAuthConfig.GetFailDelay(), ShouldEqual, 5) So(newAuthConfig.IsHtpasswdAuthEnabled(), ShouldBeTrue) // Should be updated (new path) // Should be updated by UpdateReloadableConfig So(newAuthConfig.IsAPIKeyEnabled(), ShouldBeTrue) }) Convey("Test that returned AuthConfig is isolated when config is set to nil", func() { // Create initial config with AuthConfig cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ FailDelay: 5, HTPasswd: config.AuthHTPasswd{ Path: "/etc/htpasswd", }, APIKey: false, }, }, } // Get the AuthConfig reference authConfig := cfg.CopyAuthConfig() So(authConfig, ShouldNotBeNil) So(authConfig.GetFailDelay(), ShouldEqual, 5) So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeTrue) So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse) // Set the AuthConfig to nil cfg.HTTP.Auth = nil // Verify that the returned AuthConfig is not affected by setting to nil So(authConfig, ShouldNotBeNil) // Should remain unchanged So(authConfig.GetFailDelay(), ShouldEqual, 5) // Should remain unchanged So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeTrue) // Should remain unchanged So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse) // Should remain unchanged // Verify that a new CopyAuthConfig() call returns nil newAuthConfig := cfg.CopyAuthConfig() So(newAuthConfig, ShouldBeNil) // Should be nil }) }) Convey("Test CopyAccessControlConfig()", func() { Convey("Test with non-nil AccessControl", func() { testAccessControlConfig := &config.AccessControlConfig{ Repositories: config.Repositories{ "repo1": config.PolicyGroup{ Policies: []config.Policy{ { Users: []string{"user1", "user2"}, Actions: []string{"read", "write"}, Groups: []string{"group1"}, }, }, DefaultPolicy: []string{"read"}, AnonymousPolicy: []string{"read"}, }, }, AdminPolicy: config.Policy{ Users: []string{"admin1"}, Actions: []string{"read", "write", "delete"}, Groups: []string{"admin-group"}, }, Groups: config.Groups{ "group1": config.Group{ Users: []string{"user1", "user2"}, }, }, Metrics: config.Metrics{ Users: []string{"metrics-user"}, }, } cfg := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: testAccessControlConfig, }, } accessControlConfig := cfg.CopyAccessControlConfig() So(accessControlConfig, ShouldNotBeNil) So(accessControlConfig.IsAuthzEnabled(), ShouldBeTrue) // Test deep copy isolation accessControlConfig.Repositories["repo1"].Policies[0].Users[0] = "modified-user" accessControlConfig.Repositories["repo1"].DefaultPolicy[0] = "modified-policy" accessControlConfig.AdminPolicy.Users[0] = "modified-admin" accessControlConfig.Groups["group1"].Users[0] = "modified-group-user" accessControlConfig.Metrics.Users[0] = "modified-metrics-user" // Verify original is unchanged So(cfg.HTTP.AccessControl.Repositories["repo1"].Policies[0].Users[0], ShouldEqual, "user1") So(cfg.HTTP.AccessControl.Repositories["repo1"].DefaultPolicy[0], ShouldEqual, "read") So(cfg.HTTP.AccessControl.AdminPolicy.Users[0], ShouldEqual, "admin1") So(cfg.HTTP.AccessControl.Groups["group1"].Users[0], ShouldEqual, "user1") So(cfg.HTTP.AccessControl.Metrics.Users[0], ShouldEqual, "metrics-user") }) Convey("Test with nil AccessControl", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: nil, }, } accessControlConfig := cfg.CopyAccessControlConfig() So(accessControlConfig, ShouldBeNil) }) }) Convey("Test CopyStorageConfig()", func() { Convey("Test with non-nil Storage", func() { cfg := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ RootDirectory: "/tmp/storage", GC: true, }, }, } storageConfig := cfg.CopyStorageConfig() So(storageConfig, ShouldNotBeNil) So(storageConfig.RootDirectory, ShouldEqual, "/tmp/storage") So(storageConfig.GC, ShouldBeTrue) }) Convey("Test with nil Storage", func() { cfg := &config.Config{ Storage: config.GlobalStorageConfig{}, } storageConfig := cfg.CopyStorageConfig() So(storageConfig, ShouldNotBeNil) // GlobalStorageConfig is a struct, not a pointer, so it's never nil So(storageConfig.RootDirectory, ShouldEqual, "") So(storageConfig.GC, ShouldBeFalse) }) Convey("Test StorageConfig deep copy isolation", func() { cfg := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ RootDirectory: "/tmp/storage", GC: true, Retention: config.ImageRetention{ DryRun: true, Policies: []config.RetentionPolicy{ { Repositories: []string{"repo1", "repo2"}, KeepTags: []config.KeepTagsPolicy{ { Patterns: []string{"pattern1", "pattern2"}, }, }, }, }, }, StorageDriver: map[string]any{ "type": "filesystem", }, CacheDriver: map[string]any{ "type": "redis", }, }, SubPaths: map[string]config.StorageConfig{ "/subpath1": { RootDirectory: "/tmp/subpath1", Retention: config.ImageRetention{ Policies: []config.RetentionPolicy{ { Repositories: []string{"subrepo1"}, }, }, }, StorageDriver: map[string]any{ "type": "s3", }, }, }, }, } // Get a copy of the storage config storageConfig := cfg.CopyStorageConfig() So(storageConfig, ShouldNotBeNil) // Mutate the copy's fields storageConfig.RootDirectory = "/modified/storage" storageConfig.GC = false storageConfig.Retention.Policies[0].Repositories[0] = "modified-repo" storageConfig.Retention.Policies[0].KeepTags[0].Patterns[0] = "modified-pattern" storageConfig.StorageDriver["type"] = "modified-driver" storageConfig.CacheDriver["type"] = "modified-cache" // Mutate SubPaths by getting a copy, modifying it, and putting it back subPathConfig := storageConfig.SubPaths["/subpath1"] subPathConfig.RootDirectory = "/modified/subpath1" subPathConfig.Retention.Policies[0].Repositories[0] = "modified-subrepo" subPathConfig.StorageDriver["type"] = "modified-s3" storageConfig.SubPaths["/subpath1"] = subPathConfig // Verify original config is unchanged So(cfg.Storage.RootDirectory, ShouldEqual, "/tmp/storage") So(cfg.Storage.GC, ShouldBeTrue) So(cfg.Storage.Retention.Policies[0].Repositories[0], ShouldEqual, "repo1") So(cfg.Storage.Retention.Policies[0].KeepTags[0].Patterns[0], ShouldEqual, "pattern1") So(cfg.Storage.StorageDriver["type"], ShouldEqual, "filesystem") So(cfg.Storage.CacheDriver["type"], ShouldEqual, "redis") So(cfg.Storage.SubPaths["/subpath1"].RootDirectory, ShouldEqual, "/tmp/subpath1") So(cfg.Storage.SubPaths["/subpath1"].Retention.Policies[0].Repositories[0], ShouldEqual, "subrepo1") So(cfg.Storage.SubPaths["/subpath1"].StorageDriver["type"], ShouldEqual, "s3") // Verify copy has the mutations So(storageConfig.RootDirectory, ShouldEqual, "/modified/storage") So(storageConfig.GC, ShouldBeFalse) So(storageConfig.Retention.Policies[0].Repositories[0], ShouldEqual, "modified-repo") So(storageConfig.Retention.Policies[0].KeepTags[0].Patterns[0], ShouldEqual, "modified-pattern") So(storageConfig.StorageDriver["type"], ShouldEqual, "modified-driver") So(storageConfig.CacheDriver["type"], ShouldEqual, "modified-cache") So(storageConfig.SubPaths["/subpath1"].RootDirectory, ShouldEqual, "/modified/subpath1") So(storageConfig.SubPaths["/subpath1"].Retention.Policies[0].Repositories[0], ShouldEqual, "modified-subrepo") So(storageConfig.SubPaths["/subpath1"].StorageDriver["type"], ShouldEqual, "modified-s3") }) }) Convey("Test CopyLogConfig()", func() { Convey("Test with non-nil Log", func() { cfg := &config.Config{ Log: &config.LogConfig{ Level: "info", Output: "/tmp/logs", }, } logConfig := cfg.CopyLogConfig() So(logConfig, ShouldNotBeNil) So(logConfig.Level, ShouldEqual, "info") So(logConfig.Output, ShouldEqual, "/tmp/logs") }) Convey("Test with nil Log", func() { cfg := &config.Config{ Log: nil, } logConfig := cfg.CopyLogConfig() So(logConfig, ShouldBeNil) }) }) Convey("Test CopyClusterConfig()", func() { Convey("Test with non-nil Cluster", func() { cfg := &config.Config{ Cluster: &config.ClusterConfig{ Members: []string{"node1", "node2"}, }, } clusterConfig := cfg.CopyClusterConfig() So(clusterConfig, ShouldNotBeNil) So(len(clusterConfig.Members), ShouldEqual, 2) }) Convey("Test with nil Cluster", func() { cfg := &config.Config{ Cluster: nil, } clusterConfig := cfg.CopyClusterConfig() So(clusterConfig, ShouldBeNil) }) Convey("Test ClusterConfig deep copy isolation", func() { cfg := &config.Config{ Cluster: &config.ClusterConfig{ Members: []string{"node1", "node2"}, HashKey: "test-key", TLS: &config.TLSConfig{ Cert: "test-cert", Key: "test-key", CACert: "test-ca", }, Proxy: &config.ClusterRequestProxyConfig{ LocalMemberClusterSocket: "127.0.0.1:8080", LocalMemberClusterSocketIndex: 1, }, }, } // Get a copy of the cluster config clusterConfig := cfg.CopyClusterConfig() So(clusterConfig, ShouldNotBeNil) // Mutate the copy clusterConfig.Members[0] = "modified-node" clusterConfig.HashKey = "modified-key" clusterConfig.TLS.Cert = "modified-cert" clusterConfig.Proxy.LocalMemberClusterSocket = "modified-socket" // Verify original config is unchanged So(cfg.Cluster.Members[0], ShouldEqual, "node1") So(cfg.Cluster.HashKey, ShouldEqual, "test-key") So(cfg.Cluster.TLS.Cert, ShouldEqual, "test-cert") So(cfg.Cluster.Proxy.LocalMemberClusterSocket, ShouldEqual, "127.0.0.1:8080") // Verify copy has the mutations So(clusterConfig.Members[0], ShouldEqual, "modified-node") So(clusterConfig.HashKey, ShouldEqual, "modified-key") So(clusterConfig.TLS.Cert, ShouldEqual, "modified-cert") So(clusterConfig.Proxy.LocalMemberClusterSocket, ShouldEqual, "modified-socket") }) }) Convey("Test CopySchedulerConfig()", func() { Convey("Test with non-nil Scheduler", func() { cfg := &config.Config{ Scheduler: &config.SchedulerConfig{ NumWorkers: 4, }, } schedulerConfig := cfg.CopySchedulerConfig() So(schedulerConfig, ShouldNotBeNil) So(schedulerConfig.NumWorkers, ShouldEqual, 4) }) Convey("Test with nil Scheduler", func() { cfg := &config.Config{ Scheduler: nil, } schedulerConfig := cfg.CopySchedulerConfig() So(schedulerConfig, ShouldBeNil) }) }) Convey("Test GetVersionInfo()", func() { Convey("Test with non-nil version info", func() { cfg := &config.Config{ Commit: "abc123", BinaryType: "server", GoVersion: "go1.21", DistSpecVersion: "1.1.1", } commit, binaryType, goVersion, distSpecVersion := cfg.GetVersionInfo() So(commit, ShouldEqual, "abc123") So(binaryType, ShouldEqual, "server") So(goVersion, ShouldEqual, "go1.21") So(distSpecVersion, ShouldEqual, "1.1.1") }) Convey("Test with empty version info", func() { cfg := &config.Config{ Commit: "", BinaryType: "", GoVersion: "", DistSpecVersion: "", } commit, binaryType, goVersion, distSpecVersion := cfg.GetVersionInfo() So(commit, ShouldEqual, "") So(binaryType, ShouldEqual, "") So(goVersion, ShouldEqual, "") So(distSpecVersion, ShouldEqual, "") }) }) Convey("Test GetRealm()", func() { Convey("Test with non-empty Realm", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Realm: "my-realm", }, } realm := cfg.GetRealm() So(realm, ShouldEqual, "my-realm") }) Convey("Test with empty Realm", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Realm: "", }, } realm := cfg.GetRealm() So(realm, ShouldEqual, "") }) Convey("Test with nil Config", func() { var cfg *config.Config = nil realm := cfg.GetRealm() So(realm, ShouldEqual, "") }) }) Convey("Test CopyTLSConfig()", func() { Convey("Test with non-empty TLS config", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ TLS: &config.TLSConfig{ Cert: "/path/to/cert.pem", Key: "/path/to/key.pem", CACert: "/path/to/ca.pem", }, }, } tlsConfig := cfg.CopyTLSConfig() So(tlsConfig, ShouldNotBeNil) So(tlsConfig.Cert, ShouldEqual, "/path/to/cert.pem") So(tlsConfig.Key, ShouldEqual, "/path/to/key.pem") So(tlsConfig.CACert, ShouldEqual, "/path/to/ca.pem") // Test copy isolation tlsConfig.Cert = "/modified/cert.pem" So(cfg.HTTP.TLS.Cert, ShouldEqual, "/path/to/cert.pem") }) Convey("Test with nil TLS config", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ TLS: nil, }, } tlsConfig := cfg.CopyTLSConfig() So(tlsConfig, ShouldBeNil) }) Convey("Test with nil Config", func() { var cfg *config.Config = nil tlsConfig := cfg.CopyTLSConfig() So(tlsConfig, ShouldBeNil) }) }) Convey("Test GetCompat()", func() { Convey("Test with non-empty compat config", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Compat: []compat.MediaCompatibility{ "docker2s2", "oci1", }, }, } compatConfig := cfg.GetCompat() So(compatConfig, ShouldNotBeNil) So(len(compatConfig), ShouldEqual, 2) So(string(compatConfig[0]), ShouldEqual, "docker2s2") So(string(compatConfig[1]), ShouldEqual, "oci1") // Test copy isolation compatConfig[0] = "modified-compat" So(string(cfg.HTTP.Compat[0]), ShouldEqual, "docker2s2") }) Convey("Test with nil compat config", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Compat: nil, }, } compatConfig := cfg.GetCompat() So(compatConfig, ShouldBeNil) }) Convey("Test with empty compat config", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Compat: []compat.MediaCompatibility{}, }, } compatConfig := cfg.GetCompat() So(compatConfig, ShouldNotBeNil) So(len(compatConfig), ShouldEqual, 0) }) Convey("Test with nil Config", func() { var cfg *config.Config = nil compatConfig := cfg.GetCompat() So(compatConfig, ShouldBeNil) }) }) Convey("Test GetHTTPAddress()", func() { Convey("Test with non-empty address", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Address: "192.168.1.100", }, } address := cfg.GetHTTPAddress() So(address, ShouldEqual, "192.168.1.100") }) Convey("Test with empty address", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Address: "", }, } address := cfg.GetHTTPAddress() So(address, ShouldEqual, "") }) Convey("Test with nil Config", func() { var cfg *config.Config = nil address := cfg.GetHTTPAddress() So(address, ShouldEqual, "") }) }) Convey("Test GetHTTPPort()", func() { Convey("Test with non-empty port", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Port: "8080", }, } port := cfg.GetHTTPPort() So(port, ShouldEqual, "8080") }) Convey("Test with empty port", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Port: "", }, } port := cfg.GetHTTPPort() So(port, ShouldEqual, "") }) Convey("Test with nil Config", func() { var cfg *config.Config = nil port := cfg.GetHTTPPort() So(port, ShouldEqual, "") }) }) Convey("Test GetAllowOrigin()", func() { Convey("Test with non-empty allow origin", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ AllowOrigin: "http://localhost:3000,https://example.com", }, } allowOrigin := cfg.GetAllowOrigin() So(allowOrigin, ShouldEqual, "http://localhost:3000,https://example.com") }) Convey("Test with empty allow origin", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ AllowOrigin: "", }, } allowOrigin := cfg.GetAllowOrigin() So(allowOrigin, ShouldEqual, "") }) Convey("Test with nil Config", func() { var cfg *config.Config = nil allowOrigin := cfg.GetAllowOrigin() So(allowOrigin, ShouldEqual, "") }) }) Convey("Test CopyRatelimit()", func() { Convey("Test with non-empty ratelimit config", func() { rate := 100 cfg := &config.Config{ HTTP: config.HTTPConfig{ Ratelimit: &config.RatelimitConfig{ Rate: &rate, Methods: []config.MethodRatelimitConfig{ { Method: "GET", Rate: 50, }, { Method: "POST", Rate: 25, }, }, }, }, } ratelimitConfig := cfg.CopyRatelimit() So(ratelimitConfig, ShouldNotBeNil) So(*ratelimitConfig.Rate, ShouldEqual, 100) So(len(ratelimitConfig.Methods), ShouldEqual, 2) So(ratelimitConfig.Methods[0].Method, ShouldEqual, "GET") So(ratelimitConfig.Methods[0].Rate, ShouldEqual, 50) So(ratelimitConfig.Methods[1].Method, ShouldEqual, "POST") So(ratelimitConfig.Methods[1].Rate, ShouldEqual, 25) // Test deep copy isolation *ratelimitConfig.Rate = 200 ratelimitConfig.Methods[0].Rate = 75 ratelimitConfig.Methods[0].Method = "PUT" So(*cfg.HTTP.Ratelimit.Rate, ShouldEqual, 100) So(cfg.HTTP.Ratelimit.Methods[0].Rate, ShouldEqual, 50) So(cfg.HTTP.Ratelimit.Methods[0].Method, ShouldEqual, "GET") }) Convey("Test with nil ratelimit config", func() { cfg := &config.Config{ HTTP: config.HTTPConfig{ Ratelimit: nil, }, } ratelimitConfig := cfg.CopyRatelimit() So(ratelimitConfig, ShouldBeNil) }) Convey("Test with nil Config", func() { var cfg *config.Config = nil ratelimitConfig := cfg.CopyRatelimit() So(ratelimitConfig, ShouldBeNil) }) }) }) Convey("Test Config utility methods", t, func() { Convey("Test IsMTLSAuthEnabled()", func() { // Test with nil Config var cfg *config.Config = nil So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Test with Config but no TLS cfg = &config.Config{} So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Test with Config and TLS but no client cert cfg = &config.Config{ HTTP: config.HTTPConfig{ TLS: &config.TLSConfig{ Cert: "/path/to/cert.pem", Key: "/path/to/key.pem", }, }, } So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) // Test with Config and TLS with CA cert (mTLS) cfg = &config.Config{ HTTP: config.HTTPConfig{ TLS: &config.TLSConfig{ Cert: "/path/to/cert.pem", Key: "/path/to/key.pem", CACert: "/path/to/ca-cert.pem", }, }, } So(cfg.IsMTLSAuthEnabled(), ShouldBeTrue) }) Convey("Test UseSecureSession()", func() { // Test with nil Config var cfg *config.Config = nil So(cfg.UseSecureSession(), ShouldBeFalse) // Test with Config but no TLS and no Auth cfg = &config.Config{ HTTP: config.HTTPConfig{ TLS: nil, Auth: nil, }, } So(cfg.UseSecureSession(), ShouldBeFalse) // Test with TLS configured (should return true) cfg = &config.Config{ HTTP: config.HTTPConfig{ TLS: &config.TLSConfig{ Cert: "/path/to/cert.pem", Key: "/path/to/key.pem", }, }, } So(cfg.UseSecureSession(), ShouldBeTrue) // Test with no TLS but SecureSession explicitly set to true secureTrue := true cfg = &config.Config{ HTTP: config.HTTPConfig{ TLS: nil, Auth: &config.AuthConfig{ SecureSession: &secureTrue, }, }, } So(cfg.UseSecureSession(), ShouldBeTrue) // Test with no TLS but SecureSession explicitly set to false secureFalse := false cfg = &config.Config{ HTTP: config.HTTPConfig{ TLS: nil, Auth: &config.AuthConfig{ SecureSession: &secureFalse, }, }, } So(cfg.UseSecureSession(), ShouldBeFalse) // Test with no TLS and Auth but SecureSession not set cfg = &config.Config{ HTTP: config.HTTPConfig{ TLS: nil, Auth: &config.AuthConfig{ APIKey: true, }, }, } So(cfg.UseSecureSession(), ShouldBeFalse) // Test with TLS configured and SecureSession set (TLS should take precedence) secureTrue = true cfg = &config.Config{ HTTP: config.HTTPConfig{ TLS: &config.TLSConfig{ Cert: "/path/to/cert.pem", Key: "/path/to/key.pem", }, Auth: &config.AuthConfig{ SecureSession: &secureTrue, }, }, } So(cfg.UseSecureSession(), ShouldBeTrue) // TLS takes precedence }) Convey("Test IsCompatEnabled()", func() { // Test with nil Config var cfg *config.Config = nil So(cfg.IsCompatEnabled(), ShouldBeFalse) // Test with Config but no Compat cfg = &config.Config{} So(cfg.IsCompatEnabled(), ShouldBeFalse) // Test with Config and Compat enabled cfg = &config.Config{ HTTP: config.HTTPConfig{ Compat: []compat.MediaCompatibility{compat.DockerManifestV2SchemaV2}, }, } So(cfg.IsCompatEnabled(), ShouldBeTrue) }) Convey("Test IsOpenIDSupported()", func() { // Test with unsupported provider So(config.IsOpenIDSupported("unsupported"), ShouldBeFalse) // Test with supported provider So(config.IsOpenIDSupported("google"), ShouldBeTrue) }) Convey("Test IsOauth2Supported()", func() { // Test with unsupported provider So(config.IsOauth2Supported("unsupported"), ShouldBeFalse) // Test with supported provider So(config.IsOauth2Supported("github"), ShouldBeTrue) }) Convey("Test IsClustered() with nil ClusterConfig", func() { var clusterConfig *config.ClusterConfig = nil So(clusterConfig.IsClustered(), ShouldBeFalse) }) Convey("Test IsClustered() with empty members", func() { clusterConfig := &config.ClusterConfig{ Members: []string{}, } So(clusterConfig.IsClustered(), ShouldBeFalse) }) Convey("Test IsClustered() with single member", func() { clusterConfig := &config.ClusterConfig{ Members: []string{"node1:8080"}, } So(clusterConfig.IsClustered(), ShouldBeFalse) }) Convey("Test IsClustered() with multiple members", func() { clusterConfig := &config.ClusterConfig{ Members: []string{"node1:8080", "node2:8080"}, } So(clusterConfig.IsClustered(), ShouldBeTrue) }) }) Convey("Test CopyExtensionsConfig methods", t, func() { Convey("Test IsSearchEnabled()", func() { // Test with nil Config var cfg *config.Config = nil So(cfg.CopyExtensionsConfig().IsSearchEnabled(), ShouldBeFalse) // Test with Config but nil Extensions cfg = &config.Config{} So(cfg.CopyExtensionsConfig().IsSearchEnabled(), ShouldBeFalse) // Test with Config and Extensions but nil Search cfg = &config.Config{ Extensions: &extconf.ExtensionConfig{}, } So(cfg.CopyExtensionsConfig().IsSearchEnabled(), ShouldBeFalse) // Test with Config and Extensions and Search but disabled disabled := false cfg = &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &disabled, }, }, }, } So(cfg.CopyExtensionsConfig().IsSearchEnabled(), ShouldBeFalse) // Test with Config and Extensions and Search enabled enabled := true cfg = &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, }, }, } So(cfg.CopyExtensionsConfig().IsSearchEnabled(), ShouldBeTrue) }) }) Convey("Test UpdateReloadableConfig()", t, func() { Convey("Test with nil Config", func() { var cfg *config.Config = nil newConfig := &config.Config{} So(func() { cfg.UpdateReloadableConfig(newConfig) }, ShouldNotPanic) }) Convey("Test with nil newConfig.HTTP.Auth", func() { // Create initial config with Auth cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ FailDelay: 5, HTPasswd: config.AuthHTPasswd{ Path: "/etc/htpasswd", }, APIKey: false, }, }, } // Create new config with nil Auth newConfig := &config.Config{ HTTP: config.HTTPConfig{ Auth: nil, // This should not cause a panic }, } // This should not panic even though newConfig.HTTP.Auth is nil So(func() { cfg.UpdateReloadableConfig(newConfig) }, ShouldNotPanic) // Verify that the original Auth config remains unchanged So(cfg.HTTP.Auth, ShouldNotBeNil) So(cfg.HTTP.Auth.FailDelay, ShouldEqual, 5) So(cfg.HTTP.Auth.HTPasswd.Path, ShouldEqual, "/etc/htpasswd") So(cfg.HTTP.Auth.APIKey, ShouldBeFalse) }) Convey("Test with AccessControl update", func() { cfgAccessControl := &config.AccessControlConfig{} cfgAccessControl.AdminPolicy = config.Policy{ Actions: []string{"read"}, } cfg := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: cfgAccessControl, }, } newConfigAccessControl := &config.AccessControlConfig{} newConfigAccessControl.AdminPolicy = config.Policy{ Actions: []string{"read", "write"}, } newConfig := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: newConfigAccessControl, }, } cfg.UpdateReloadableConfig(newConfig) So(cfg.CopyAccessControlConfig().GetAdminPolicy().Actions, ShouldResemble, []string{"read", "write"}) }) Convey("Test with Extensions update", func() { // First set up a config with search enabled enabled := true cfg := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, }, }, } // Create new config with CVE config newConfig := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, CVE: &extconf.CVEConfig{ UpdateInterval: time.Hour * 2, }, }, }, } cfg.UpdateReloadableConfig(newConfig) // The search should still be enabled and CVE config should be updated So(cfg.CopyExtensionsConfig().IsSearchEnabled(), ShouldBeTrue) }) Convey("Test search CVE config removal when new config has nil Search.CVE", func() { // First set up a config with search enabled and CVE config enabled := true cfg := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, CVE: &extconf.CVEConfig{ UpdateInterval: time.Hour, }, }, }, } So(cfg.CopyExtensionsConfig().IsSearchEnabled(), ShouldBeTrue) So(cfg.Extensions.Search.CVE, ShouldNotBeNil) // Create new config with Search but nil CVE newConfig := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, CVE: nil, // This should trigger the removal }, }, } cfg.UpdateReloadableConfig(newConfig) // Verify that the CVE config was removed So(cfg.Extensions.Search.CVE, ShouldBeNil) So(cfg.Extensions.Search.Enable, ShouldNotBeNil) So(*cfg.Extensions.Search.Enable, ShouldBeTrue) }) Convey("Test search CVE config removal when new config has nil Search", func() { // First set up a config with search enabled and CVE config enabled := true cfg := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, CVE: &extconf.CVEConfig{ UpdateInterval: time.Hour, }, }, }, } So(cfg.CopyExtensionsConfig().IsSearchEnabled(), ShouldBeTrue) So(cfg.Extensions.Search.CVE, ShouldNotBeNil) // Create new config with Extensions but nil Search newConfig := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: nil, // This should trigger the removal }, } cfg.UpdateReloadableConfig(newConfig) // Verify that the CVE config was removed So(cfg.Extensions.Search.CVE, ShouldBeNil) So(cfg.Extensions.Search.Enable, ShouldNotBeNil) So(*cfg.Extensions.Search.Enable, ShouldBeTrue) }) }) Convey("Test nil receiver coverage for all methods", t, func() { Convey("Test AuthConfig methods with nil receiver", func() { var authConfig *config.AuthConfig = nil So(authConfig.IsLdapAuthEnabled(), ShouldBeFalse) So(authConfig.IsHtpasswdAuthEnabled(), ShouldBeFalse) So(authConfig.IsBearerAuthEnabled(), ShouldBeFalse) So(authConfig.IsOpenIDAuthEnabled(), ShouldBeFalse) So(authConfig.IsAPIKeyEnabled(), ShouldBeFalse) So(authConfig.IsBasicAuthnEnabled(), ShouldBeFalse) So(authConfig.GetFailDelay(), ShouldEqual, 0) }) Convey("Test LDAPConfig methods with nil receiver", func() { var ldapConfig *config.LDAPConfig = nil So(ldapConfig.BindDN(), ShouldEqual, "") So(ldapConfig.BindPassword(), ShouldEqual, "") So(ldapConfig.SetBindDN("test"), ShouldBeNil) So(ldapConfig.SetBindPassword("test"), ShouldBeNil) }) Convey("Test AccessControlConfig methods with nil receiver", func() { var accessControlConfig *config.AccessControlConfig = nil So(accessControlConfig.IsAuthzEnabled(), ShouldBeFalse) So(accessControlConfig.AnonymousPolicyExists(), ShouldBeFalse) So(accessControlConfig.ContainsOnlyAnonymousPolicy(), ShouldBeTrue) // Test getter methods So(accessControlConfig.GetRepositories(), ShouldBeNil) So(accessControlConfig.GetAdminPolicy(), ShouldResemble, config.Policy{}) So(accessControlConfig.GetMetrics(), ShouldResemble, config.Metrics{}) So(accessControlConfig.GetGroups(), ShouldBeNil) }) Convey("Test Config methods with nil receiver", func() { var cfg *config.Config = nil // Test getter methods So(cfg.CopyAuthConfig(), ShouldBeNil) So(cfg.CopyAccessControlConfig(), ShouldBeNil) So(cfg.GetHTTPAddress(), ShouldEqual, "") So(cfg.GetHTTPPort(), ShouldEqual, "") So(cfg.GetAllowOrigin(), ShouldEqual, "") So(cfg.CopyTLSConfig(), ShouldBeNil) So(cfg.CopyRatelimit(), ShouldBeNil) So(cfg.GetCompat(), ShouldBeNil) So(cfg.CopyStorageConfig(), ShouldResemble, config.GlobalStorageConfig{}) So(cfg.CopyExtensionsConfig(), ShouldBeNil) So(cfg.CopyLogConfig(), ShouldBeNil) So(cfg.CopyClusterConfig(), ShouldBeNil) So(cfg.CopySchedulerConfig(), ShouldBeNil) // Test GetVersionInfo commit, binaryType, goVersion, distSpecVersion := cfg.GetVersionInfo() So(commit, ShouldEqual, "") So(binaryType, ShouldEqual, "") So(goVersion, ShouldEqual, "") So(distSpecVersion, ShouldEqual, "") // Test boolean methods So(cfg.IsMTLSAuthEnabled(), ShouldBeFalse) So(cfg.IsRetentionEnabled(), ShouldBeFalse) So(cfg.IsCompatEnabled(), ShouldBeFalse) // Test Sanitize So(cfg.Sanitize(), ShouldBeNil) // Test UpdateReloadableConfig (should not panic) newConfig := &config.Config{} So(func() { cfg.UpdateReloadableConfig(newConfig) }, ShouldNotPanic) }) }) Convey("Test AccessControlConfig copy isolation through CopyAccessControlConfig()", t, func() { Convey("Test that mutations to retrieved AccessControlConfig copy do not affect original config", func() { // Create a config with initial AccessControlConfig cfg := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: &config.AccessControlConfig{ AdminPolicy: config.Policy{ Actions: []string{"read"}, Users: []string{"admin"}, }, Repositories: config.Repositories{ "repo1": config.PolicyGroup{ DefaultPolicy: []string{"read"}, Policies: []config.Policy{ { Actions: []string{"read"}, }, }, }, }, }, }, } // Retrieve the AccessControlConfig (should be a copy) accessControlConfig := cfg.CopyAccessControlConfig() So(accessControlConfig, ShouldNotBeNil) // Mutate the retrieved AccessControlConfig copy accessControlConfig.AdminPolicy = config.Policy{ Actions: []string{"read", "write", "delete"}, Users: []string{"admin", "superadmin"}, } // Add a new repository to the copy newRepositories := config.Repositories{ "repo1": config.PolicyGroup{ DefaultPolicy: []string{"read"}, Policies: []config.Policy{ { Actions: []string{"read"}, }, }, }, "repo2": config.PolicyGroup{ DefaultPolicy: []string{"read", "write"}, Policies: []config.Policy{ { Actions: []string{"read", "write"}, Users: []string{"user1"}, }, }, }, } accessControlConfig.Repositories = newRepositories // Verify that the original config is unchanged originalAccessControlConfig := cfg.CopyAccessControlConfig() So(originalAccessControlConfig, ShouldNotBeNil) // Check that admin policy remains unchanged in original adminPolicy := originalAccessControlConfig.GetAdminPolicy() So(adminPolicy.Actions, ShouldResemble, []string{"read"}) So(adminPolicy.Users, ShouldResemble, []string{"admin"}) // Check that repositories remain unchanged in original repositories := originalAccessControlConfig.GetRepositories() So(len(repositories), ShouldEqual, 1) So(repositories["repo1"], ShouldNotBeNil) So(repositories["repo1"].DefaultPolicy, ShouldResemble, []string{"read"}) }) Convey("Test that mutations to retrieved AccessControlConfig copy work with nil initial config", func() { // Create a config with nil AccessControlConfig cfg := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: nil, }, } // Retrieve the AccessControlConfig (should return nil) accessControlConfig := cfg.CopyAccessControlConfig() So(accessControlConfig, ShouldBeNil) // Create a new AccessControlConfig and set it newAccessControlConfig := &config.AccessControlConfig{} newAccessControlConfig.AdminPolicy = config.Policy{ Actions: []string{"read"}, Users: []string{"admin"}, } // Manually set the AccessControlConfig on the original config cfg.HTTP.AccessControl = newAccessControlConfig // Now retrieve it again and verify it works retrievedConfig := cfg.CopyAccessControlConfig() So(retrievedConfig, ShouldNotBeNil) // Mutate the retrieved config copy retrievedConfig.AdminPolicy = config.Policy{ Actions: []string{"read", "write"}, Users: []string{"admin", "user"}, } // Verify the original config is unchanged finalConfig := cfg.CopyAccessControlConfig() adminPolicy := finalConfig.GetAdminPolicy() So(adminPolicy.Actions, ShouldResemble, []string{"read"}) So(adminPolicy.Users, ShouldResemble, []string{"admin"}) }) }) Convey("Test AccessControlConfig copy isolation through UpdateReloadableConfig()", t, func() { Convey("Test that AccessControlConfig copies are isolated from UpdateReloadableConfig changes", func() { // Create initial config with AccessControlConfig cfg := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: &config.AccessControlConfig{ AdminPolicy: config.Policy{ Actions: []string{"read"}, Users: []string{"admin"}, }, Repositories: config.Repositories{ "repo1": config.PolicyGroup{ DefaultPolicy: []string{"read"}, Policies: []config.Policy{ { Actions: []string{"read"}, }, }, }, }, }, }, } // Get initial reference to AccessControlConfig initialAccessControlConfig := cfg.CopyAccessControlConfig() So(initialAccessControlConfig, ShouldNotBeNil) // Verify initial state initialAdminPolicy := initialAccessControlConfig.GetAdminPolicy() So(initialAdminPolicy.Actions, ShouldResemble, []string{"read"}) So(initialAdminPolicy.Users, ShouldResemble, []string{"admin"}) initialRepositories := initialAccessControlConfig.GetRepositories() So(len(initialRepositories), ShouldEqual, 1) So(initialRepositories["repo1"], ShouldNotBeNil) // Create new config with updated AccessControlConfig newConfig := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: &config.AccessControlConfig{ AdminPolicy: config.Policy{ Actions: []string{"read", "write", "delete"}, Users: []string{"admin", "superadmin", "user"}, }, Repositories: config.Repositories{ "repo1": config.PolicyGroup{ DefaultPolicy: []string{"read", "write"}, Policies: []config.Policy{ { Actions: []string{"read", "write"}, }, }, }, "repo2": config.PolicyGroup{ DefaultPolicy: []string{"read"}, Policies: []config.Policy{ { Actions: []string{"read"}, Users: []string{"user1", "user2"}, }, }, }, }, }, }, } // Update the config using UpdateReloadableConfig cfg.UpdateReloadableConfig(newConfig) // Verify that the old copy remains unchanged (copy isolation) updatedAdminPolicy := initialAccessControlConfig.GetAdminPolicy() So(updatedAdminPolicy.Actions, ShouldResemble, []string{"read"}) So(updatedAdminPolicy.Users, ShouldResemble, []string{"admin"}) updatedRepositories := initialAccessControlConfig.GetRepositories() So(len(updatedRepositories), ShouldEqual, 1) So(updatedRepositories["repo1"], ShouldNotBeNil) So(updatedRepositories["repo1"].DefaultPolicy, ShouldResemble, []string{"read"}) // Verify that a new copy gets the updated data newAccessControlConfig := cfg.CopyAccessControlConfig() So(newAccessControlConfig, ShouldNotBeNil) So(newAccessControlConfig, ShouldNotEqual, initialAccessControlConfig) // Different copy newAdminPolicy := newAccessControlConfig.GetAdminPolicy() So(newAdminPolicy.Actions, ShouldResemble, []string{"read", "write", "delete"}) So(newAdminPolicy.Users, ShouldResemble, []string{"admin", "superadmin", "user"}) }) Convey("Test that old AccessControlConfig reference works with nil initial config", func() { // Create config with nil AccessControlConfig cfg := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: nil, }, } // Get initial reference (should be nil) initialAccessControlConfig := cfg.CopyAccessControlConfig() So(initialAccessControlConfig, ShouldBeNil) // Create new config with AccessControlConfig newConfig := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: &config.AccessControlConfig{ AdminPolicy: config.Policy{ Actions: []string{"read", "write"}, Users: []string{"admin"}, }, }, }, } // Update the config using UpdateReloadableConfig cfg.UpdateReloadableConfig(newConfig) // Verify that a new reference now gets the data newAccessControlConfig := cfg.CopyAccessControlConfig() So(newAccessControlConfig, ShouldNotBeNil) adminPolicy := newAccessControlConfig.GetAdminPolicy() So(adminPolicy.Actions, ShouldResemble, []string{"read", "write"}) So(adminPolicy.Users, ShouldResemble, []string{"admin"}) }) Convey("Test that old AccessControlConfig reference works when new config has nil AccessControlConfig", func() { // Create initial config with AccessControlConfig testAccessControlConfig := &config.AccessControlConfig{} testAccessControlConfig.AdminPolicy = config.Policy{ Actions: []string{"read"}, Users: []string{"admin"}, } cfg := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: testAccessControlConfig, }, } // Get initial reference initialAccessControlConfig := cfg.CopyAccessControlConfig() So(initialAccessControlConfig, ShouldNotBeNil) // Create new config with nil AccessControlConfig newConfig := &config.Config{ HTTP: config.HTTPConfig{ AccessControl: nil, }, } // Update the config using UpdateReloadableConfig cfg.UpdateReloadableConfig(newConfig) // Verify that a new reference now returns nil newAccessControlConfig := cfg.CopyAccessControlConfig() So(newAccessControlConfig, ShouldBeNil) }) }) Convey("Test ExtensionConfig copy isolation through CopyExtensionsConfig()", t, func() { Convey("Test that mutations to retrieved ExtensionConfig copy do not affect original config", func() { // Create a config with initial ExtensionConfig enabled := true cfg := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, CVE: &extconf.CVEConfig{ UpdateInterval: time.Hour, Trivy: &extconf.TrivyConfig{ DBRepository: "original/trivy-db", }, }, }, Sync: &syncconf.Config{ Enable: &enabled, Registries: []syncconf.RegistryConfig{ { URLs: []string{"http://original:5000"}, }, }, }, Metrics: &extconf.MetricsConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, Prometheus: &extconf.PrometheusConfig{ Path: "/metrics", }, }, Scrub: &extconf.ScrubConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, Interval: 24 * time.Hour, }, UI: &extconf.UIConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, }, }, } // Retrieve the ExtensionConfig extensionConfig := cfg.CopyExtensionsConfig() So(extensionConfig, ShouldNotBeNil) // Mutate the retrieved ExtensionConfig copy disabled := false extensionConfig.Search.Enable = &disabled extensionConfig.Search.CVE.UpdateInterval = 2 * time.Hour extensionConfig.Search.CVE.Trivy.DBRepository = "modified/trivy-db" extensionConfig.Sync.Registries[0].URLs[0] = "http://modified:5000" extensionConfig.Metrics.Prometheus.Path = "/custom/metrics" extensionConfig.Scrub.Interval = 48 * time.Hour extensionConfig.UI.Enable = &disabled // Verify that the original config is unchanged So(*cfg.Extensions.Search.Enable, ShouldBeTrue) So(cfg.Extensions.Search.CVE.UpdateInterval, ShouldEqual, time.Hour) So(cfg.Extensions.Search.CVE.Trivy.DBRepository, ShouldEqual, "original/trivy-db") So(cfg.Extensions.Sync.Registries[0].URLs[0], ShouldEqual, "http://original:5000") So(cfg.Extensions.Metrics.Prometheus.Path, ShouldEqual, "/metrics") So(cfg.Extensions.Scrub.Interval, ShouldEqual, 24*time.Hour) So(*cfg.Extensions.UI.Enable, ShouldBeTrue) // Verify that the retrieved config has the mutations So(*extensionConfig.Search.Enable, ShouldBeFalse) So(extensionConfig.Search.CVE.UpdateInterval, ShouldEqual, 2*time.Hour) So(extensionConfig.Search.CVE.Trivy.DBRepository, ShouldEqual, "modified/trivy-db") So(extensionConfig.Sync.Registries[0].URLs[0], ShouldEqual, "http://modified:5000") So(extensionConfig.Metrics.Prometheus.Path, ShouldEqual, "/custom/metrics") So(extensionConfig.Scrub.Interval, ShouldEqual, 48*time.Hour) So(*extensionConfig.UI.Enable, ShouldBeFalse) }) Convey("Test that mutations to retrieved ExtensionConfig work with nil initial config", func() { // Create a config with nil ExtensionConfig cfg := &config.Config{ Extensions: nil, } // Retrieve the ExtensionConfig (should return nil) extensionConfig := cfg.CopyExtensionsConfig() So(extensionConfig, ShouldBeNil) // Create a new ExtensionConfig and set it enabled := true newExtensionConfig := &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, }, Metrics: &extconf.MetricsConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, Prometheus: &extconf.PrometheusConfig{ Path: "/metrics", }, }, } // Manually set the ExtensionConfig on the original config cfg.Extensions = newExtensionConfig // Now retrieve it again and verify it works retrievedConfig := cfg.CopyExtensionsConfig() So(retrievedConfig, ShouldNotBeNil) // Mutate the retrieved config retrievedConfig.Metrics.Prometheus.Path = "/new/metrics" // Verify the changes are NOT reflected in original config finalConfig := cfg.CopyExtensionsConfig() So(finalConfig.Metrics.Prometheus.Path, ShouldEqual, "/metrics") }) }) Convey("Test ExtensionConfig copy isolation through UpdateReloadableConfig()", t, func() { Convey("Test that ExtensionConfig copies are isolated from UpdateReloadableConfig changes", func() { // Create initial config with ExtensionConfig enabled := true cfg := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, }, Metrics: &extconf.MetricsConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, Prometheus: &extconf.PrometheusConfig{ Path: "/metrics", }, }, }, } // Get initial reference to ExtensionConfig initialExtensionConfig := cfg.CopyExtensionsConfig() So(initialExtensionConfig, ShouldNotBeNil) // Verify initial state So(initialExtensionConfig.Metrics.Prometheus.Path, ShouldEqual, "/metrics") So(initialExtensionConfig.Sync, ShouldBeNil) So(initialExtensionConfig.Search.CVE, ShouldBeNil) So(initialExtensionConfig.Scrub, ShouldBeNil) // Create new config with updated ExtensionConfig newConfig := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, CVE: &extconf.CVEConfig{ UpdateInterval: time.Hour * 2, Trivy: &extconf.TrivyConfig{ DBRepository: "updated/trivy-db", }, }, }, Metrics: &extconf.MetricsConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, Prometheus: &extconf.PrometheusConfig{ Path: "/custom/metrics", }, }, Sync: &syncconf.Config{ Enable: &enabled, Registries: []syncconf.RegistryConfig{ { URLs: []string{"http://registry1:5000", "http://registry2:5000"}, }, }, }, Scrub: &extconf.ScrubConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, Interval: time.Hour * 12, }, }, } // Update the config using UpdateReloadableConfig cfg.UpdateReloadableConfig(newConfig) // Verify that the old reference remains unchanged (copy isolation) So(initialExtensionConfig.Metrics.Prometheus.Path, ShouldEqual, "/metrics") So(initialExtensionConfig.Sync, ShouldBeNil) So(initialExtensionConfig.Search.CVE, ShouldBeNil) So(initialExtensionConfig.Scrub, ShouldBeNil) // Verify that a new reference gets the updated data newExtensionConfig := cfg.CopyExtensionsConfig() So(newExtensionConfig, ShouldNotBeNil) So(newExtensionConfig, ShouldNotEqual, initialExtensionConfig) // Different references So(newExtensionConfig.Metrics.Prometheus.Path, ShouldEqual, "/metrics") So(newExtensionConfig.Sync, ShouldNotBeNil) So(newExtensionConfig.Search.CVE, ShouldNotBeNil) So(newExtensionConfig.Scrub, ShouldNotBeNil) }) Convey("Test that old ExtensionConfig reference works with nil initial config", func() { // Create config with nil ExtensionConfig cfg := &config.Config{ Extensions: nil, } // Get initial reference (should be nil) initialExtensionConfig := cfg.CopyExtensionsConfig() So(initialExtensionConfig, ShouldBeNil) // Create new config with ExtensionConfig enabled := true newConfig := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, }, Metrics: &extconf.MetricsConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, Prometheus: &extconf.PrometheusConfig{ Path: "/new/metrics", }, }, }, } // Update the config using UpdateReloadableConfig cfg.UpdateReloadableConfig(newConfig) // Verify that a new reference now gets the data newExtensionConfig := cfg.CopyExtensionsConfig() So(newExtensionConfig, ShouldNotBeNil) // Note: UpdateReloadableConfig creates an empty ExtensionConfig when going from nil to non-nil, // but doesn't copy the fields from newConfig.Extensions. It only updates specific parts. // So the Search and Metrics fields will be nil in the new ExtensionConfig. So(newExtensionConfig.Search, ShouldBeNil) So(newExtensionConfig.Metrics, ShouldBeNil) }) Convey("Test that old ExtensionConfig reference works when new config has nil ExtensionConfig", func() { // Create initial config with ExtensionConfig enabled := true cfg := &config.Config{ Extensions: &extconf.ExtensionConfig{ Search: &extconf.SearchConfig{ BaseConfig: extconf.BaseConfig{ Enable: &enabled, }, }, }, } // Get initial reference initialExtensionConfig := cfg.CopyExtensionsConfig() So(initialExtensionConfig, ShouldNotBeNil) // Create new config with nil ExtensionConfig newConfig := &config.Config{ Extensions: nil, } // Update the config using UpdateReloadableConfig cfg.UpdateReloadableConfig(newConfig) // Verify that the old reference remains unchanged (copy isolation) So(initialExtensionConfig, ShouldNotBeNil) So(initialExtensionConfig.Search, ShouldNotBeNil) // Verify that a new reference now returns nil newExtensionConfig := cfg.CopyExtensionsConfig() So(newExtensionConfig, ShouldBeNil) }) }) Convey("Test UpdateReloadableConfig LDAP config updates", t, func() { Convey("Test LDAP config is updated in UpdateReloadableConfig", func() { // Create initial config with LDAP cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ LDAP: &config.LDAPConfig{ Address: "ldap://old-server:389", Port: 389, Insecure: true, }, }, }, } // Create new config with updated LDAP newConfig := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ LDAP: &config.LDAPConfig{ Address: "ldap://new-server:636", Port: 636, Insecure: false, StartTLS: true, }, }, }, } // Update the config cfg.UpdateReloadableConfig(newConfig) // Verify LDAP config was updated So(cfg.HTTP.Auth.LDAP, ShouldNotBeNil) So(cfg.HTTP.Auth.LDAP.Address, ShouldEqual, "ldap://new-server:636") So(cfg.HTTP.Auth.LDAP.Port, ShouldEqual, 636) So(cfg.HTTP.Auth.LDAP.Insecure, ShouldBeFalse) So(cfg.HTTP.Auth.LDAP.StartTLS, ShouldBeTrue) }) Convey("Test LDAP config is set to nil when new config has nil LDAP", func() { // Create initial config with LDAP cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ LDAP: &config.LDAPConfig{ Address: "ldap://old-server:389", }, }, }, } // Create new config with nil LDAP newConfig := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ LDAP: nil, }, }, } // Update the config cfg.UpdateReloadableConfig(newConfig) // Verify LDAP config was set to nil So(cfg.HTTP.Auth.LDAP, ShouldBeNil) }) Convey("Test LDAP config is created when going from nil to non-nil", func() { // Create initial config with nil LDAP cfg := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ LDAP: nil, }, }, } // Create new config with LDAP newConfig := &config.Config{ HTTP: config.HTTPConfig{ Auth: &config.AuthConfig{ LDAP: &config.LDAPConfig{ Address: "ldap://new-server:389", Port: 389, }, }, }, } // Update the config cfg.UpdateReloadableConfig(newConfig) // Verify LDAP config was created So(cfg.HTTP.Auth.LDAP, ShouldNotBeNil) So(cfg.HTTP.Auth.LDAP.Address, ShouldEqual, "ldap://new-server:389") So(cfg.HTTP.Auth.LDAP.Port, ShouldEqual, 389) }) }) Convey("Test UpdateReloadableConfig Storage.SubPaths logic", t, func() { Convey("Test existing SubPaths are updated", func() { // Create initial config with SubPaths cfg := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: true, Dedupe: false, GCDelay: time.Hour, GCInterval: time.Hour * 24, }, "/path2": { GC: false, Dedupe: true, GCDelay: time.Hour * 2, GCInterval: time.Hour * 48, }, }, }, } // Create new config with updated SubPaths newConfig := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: false, // Changed Dedupe: true, // Changed GCDelay: time.Hour * 2, // Changed GCInterval: time.Hour * 12, // Changed }, "/path2": { GC: true, // Changed Dedupe: false, // Changed GCDelay: time.Hour * 3, // Changed GCInterval: time.Hour * 36, // Changed }, }, }, } // Update the config cfg.UpdateReloadableConfig(newConfig) // Verify SubPaths were updated So(len(cfg.Storage.SubPaths), ShouldEqual, 2) // Check /path1 path1Config := cfg.Storage.SubPaths["/path1"] So(path1Config.GC, ShouldBeFalse) So(path1Config.Dedupe, ShouldBeTrue) So(path1Config.GCDelay, ShouldEqual, time.Hour*2) So(path1Config.GCInterval, ShouldEqual, time.Hour*12) // Check /path2 path2Config := cfg.Storage.SubPaths["/path2"] So(path2Config.GC, ShouldBeTrue) So(path2Config.Dedupe, ShouldBeFalse) So(path2Config.GCDelay, ShouldEqual, time.Hour*3) So(path2Config.GCInterval, ShouldEqual, time.Hour*36) }) Convey("Test new SubPaths are not added (only existing ones are updated)", func() { // Create initial config with one SubPath cfg := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: true, Dedupe: false, }, }, }, } // Create new config with additional SubPath newConfig := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: false, // Update existing Dedupe: true, // Update existing }, "/path2": { // New path - should not be added GC: true, Dedupe: true, }, }, }, } // Update the config cfg.UpdateReloadableConfig(newConfig) // Verify only existing SubPath was updated, new one was not added So(len(cfg.Storage.SubPaths), ShouldEqual, 1) _, exists := cfg.Storage.SubPaths["/path2"] So(exists, ShouldBeFalse) // New path not added // Verify existing path was updated path1Config := cfg.Storage.SubPaths["/path1"] So(path1Config.GC, ShouldBeFalse) So(path1Config.Dedupe, ShouldBeTrue) }) Convey("Test SubPaths Retention is updated only when retention is enabled", func() { // Create initial config with retention enabled and SubPaths // Retention is enabled when there are policies with tag retention cfg := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, Retention: config.ImageRetention{ Policies: []config.RetentionPolicy{ { Repositories: []string{"repo1"}, KeepTags: []config.KeepTagsPolicy{ { MostRecentlyPulledCount: 10, // This enables retention }, }, }, }, }, }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: true, Dedupe: false, Retention: config.ImageRetention{ Policies: []config.RetentionPolicy{ { Repositories: []string{"old-repo"}, }, }, }, }, }, }, } // Create new config with updated SubPath retention newConfig := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, Retention: config.ImageRetention{ Policies: []config.RetentionPolicy{ { Repositories: []string{"repo1"}, KeepTags: []config.KeepTagsPolicy{ { MostRecentlyPulledCount: 10, // This enables retention }, }, }, }, }, }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: false, Dedupe: true, Retention: config.ImageRetention{ Policies: []config.RetentionPolicy{ { Repositories: []string{"new-repo"}, }, }, }, }, }, }, } // Update the config cfg.UpdateReloadableConfig(newConfig) // Verify SubPath was updated including Retention path1Config := cfg.Storage.SubPaths["/path1"] So(path1Config.GC, ShouldBeFalse) So(path1Config.Dedupe, ShouldBeTrue) So(len(path1Config.Retention.Policies), ShouldEqual, 1) So(path1Config.Retention.Policies[0].Repositories[0], ShouldEqual, "new-repo") }) Convey("Test SubPaths Retention is not updated when retention is disabled", func() { // Create initial config with retention disabled and SubPaths cfg := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, // No Retention config - retention disabled }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: true, Dedupe: false, Retention: config.ImageRetention{ Policies: []config.RetentionPolicy{ { Repositories: []string{"old-repo"}, }, }, }, }, }, }, } // Create new config with updated SubPath retention newConfig := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, // No Retention config - retention disabled }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: false, Dedupe: true, Retention: config.ImageRetention{ Policies: []config.RetentionPolicy{ { Repositories: []string{"new-repo"}, }, }, }, }, }, }, } // Update the config cfg.UpdateReloadableConfig(newConfig) // Verify SubPath was updated but Retention was not path1Config := cfg.Storage.SubPaths["/path1"] So(path1Config.GC, ShouldBeFalse) So(path1Config.Dedupe, ShouldBeTrue) // Retention should remain unchanged (old value) So(len(path1Config.Retention.Policies), ShouldEqual, 1) So(path1Config.Retention.Policies[0].Repositories[0], ShouldEqual, "old-repo") }) Convey("Test SubPaths with empty new config", func() { // Create initial config with SubPaths cfg := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, }, SubPaths: map[string]config.StorageConfig{ "/path1": { GC: true, Dedupe: false, }, "/path2": { GC: false, Dedupe: true, }, }, }, } // Create new config with empty SubPaths newConfig := &config.Config{ Storage: config.GlobalStorageConfig{ StorageConfig: config.StorageConfig{ GC: true, Dedupe: false, }, SubPaths: map[string]config.StorageConfig{}, }, } // Update the config cfg.UpdateReloadableConfig(newConfig) // Verify existing SubPaths remain unchanged (no updates applied) So(len(cfg.Storage.SubPaths), ShouldEqual, 2) path1Config := cfg.Storage.SubPaths["/path1"] So(path1Config.GC, ShouldBeTrue) // Unchanged So(path1Config.Dedupe, ShouldBeFalse) // Unchanged path2Config := cfg.Storage.SubPaths["/path2"] So(path2Config.GC, ShouldBeFalse) // Unchanged So(path2Config.Dedupe, ShouldBeTrue) // Unchanged }) }) }