using Microsoft.Extensions.Configuration; using Shouldly; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host.Configuration; namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration { /// /// Verifies that application configuration binds correctly from appsettings and that validation catches invalid bridge settings. /// public class ConfigurationLoadingTests { /// /// Loads the application configuration from the repository appsettings file for binding tests. /// /// The bound application configuration snapshot. private static AppConfiguration LoadFromJson() { var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: false) .Build(); var config = new AppConfiguration(); configuration.GetSection("OpcUa").Bind(config.OpcUa); configuration.GetSection("MxAccess").Bind(config.MxAccess); configuration.GetSection("GalaxyRepository").Bind(config.GalaxyRepository); configuration.GetSection("Dashboard").Bind(config.Dashboard); configuration.GetSection("Security").Bind(config.Security); return config; } /// /// Confirms that the OPC UA section binds the endpoint and session settings expected by the bridge. /// [Fact] public void OpcUa_Section_BindsCorrectly() { var config = LoadFromJson(); config.OpcUa.BindAddress.ShouldBe("0.0.0.0"); config.OpcUa.Port.ShouldBe(4840); config.OpcUa.EndpointPath.ShouldBe("/LmxOpcUa"); config.OpcUa.ServerName.ShouldBe("LmxOpcUa"); config.OpcUa.GalaxyName.ShouldBe("ZB"); config.OpcUa.MaxSessions.ShouldBe(100); config.OpcUa.SessionTimeoutMinutes.ShouldBe(30); } /// /// Confirms that the MXAccess section binds runtime timeout and reconnect settings correctly. /// [Fact] public void MxAccess_Section_BindsCorrectly() { var config = LoadFromJson(); config.MxAccess.ClientName.ShouldBe("LmxOpcUa"); config.MxAccess.ReadTimeoutSeconds.ShouldBe(5); config.MxAccess.WriteTimeoutSeconds.ShouldBe(5); config.MxAccess.MaxConcurrentOperations.ShouldBe(10); config.MxAccess.MonitorIntervalSeconds.ShouldBe(5); config.MxAccess.AutoReconnect.ShouldBe(true); config.MxAccess.ProbeStaleThresholdSeconds.ShouldBe(60); } /// /// Confirms that the Galaxy repository section binds connection and polling settings correctly. /// [Fact] public void GalaxyRepository_Section_BindsCorrectly() { var config = LoadFromJson(); config.GalaxyRepository.ConnectionString.ShouldContain("ZB"); config.GalaxyRepository.ChangeDetectionIntervalSeconds.ShouldBe(30); config.GalaxyRepository.CommandTimeoutSeconds.ShouldBe(30); config.GalaxyRepository.ExtendedAttributes.ShouldBe(false); } /// /// Confirms that extended-attribute loading defaults to disabled when not configured. /// [Fact] public void GalaxyRepository_ExtendedAttributes_DefaultsFalse() { var config = new GalaxyRepositoryConfiguration(); config.ExtendedAttributes.ShouldBe(false); } /// /// Confirms that the extended-attribute flag can be enabled through configuration binding. /// [Fact] public void GalaxyRepository_ExtendedAttributes_BindsFromJson() { var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: false) .AddInMemoryCollection(new[] { new System.Collections.Generic.KeyValuePair("GalaxyRepository:ExtendedAttributes", "true") }) .Build(); var config = new GalaxyRepositoryConfiguration(); configuration.GetSection("GalaxyRepository").Bind(config); config.ExtendedAttributes.ShouldBe(true); } /// /// Confirms that the dashboard section binds operator-dashboard settings correctly. /// [Fact] public void Dashboard_Section_BindsCorrectly() { var config = LoadFromJson(); config.Dashboard.Enabled.ShouldBe(true); config.Dashboard.Port.ShouldBe(8081); config.Dashboard.RefreshIntervalSeconds.ShouldBe(10); } /// /// Confirms that the default configuration objects start with the expected bridge defaults. /// [Fact] public void DefaultValues_AreCorrect() { var config = new AppConfiguration(); config.OpcUa.BindAddress.ShouldBe("0.0.0.0"); config.OpcUa.Port.ShouldBe(4840); config.MxAccess.ClientName.ShouldBe("LmxOpcUa"); config.GalaxyRepository.ChangeDetectionIntervalSeconds.ShouldBe(30); config.Dashboard.Enabled.ShouldBe(true); } /// /// Confirms that BindAddress can be overridden to a specific hostname or IP. /// [Fact] public void OpcUa_BindAddress_CanBeOverridden() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new[] { new System.Collections.Generic.KeyValuePair("OpcUa:BindAddress", "localhost"), }) .Build(); var config = new OpcUaConfiguration(); configuration.GetSection("OpcUa").Bind(config); config.BindAddress.ShouldBe("localhost"); } /// /// Confirms that a valid configuration passes startup validation. /// [Fact] public void Validator_ValidConfig_ReturnsTrue() { var config = LoadFromJson(); ConfigurationValidator.ValidateAndLog(config).ShouldBe(true); } /// /// Confirms that an invalid OPC UA port is rejected by startup validation. /// [Fact] public void Validator_InvalidPort_ReturnsFalse() { var config = new AppConfiguration(); config.OpcUa.Port = 0; ConfigurationValidator.ValidateAndLog(config).ShouldBe(false); } /// /// Confirms that an empty Galaxy name is rejected because the bridge requires a namespace target. /// [Fact] public void Validator_EmptyGalaxyName_ReturnsFalse() { var config = new AppConfiguration(); config.OpcUa.GalaxyName = ""; ConfigurationValidator.ValidateAndLog(config).ShouldBe(false); } /// /// Confirms that the Security section binds profile list from appsettings.json. /// [Fact] public void Security_Section_BindsProfilesCorrectly() { var config = LoadFromJson(); config.Security.Profiles.ShouldContain("None"); config.Security.AutoAcceptClientCertificates.ShouldBe(true); config.Security.MinimumCertificateKeySize.ShouldBe(2048); } /// /// Confirms that a minimum key size below 2048 is rejected by the validator. /// [Fact] public void Validator_InvalidMinKeySize_ReturnsFalse() { var config = new AppConfiguration(); config.Security.MinimumCertificateKeySize = 1024; ConfigurationValidator.ValidateAndLog(config).ShouldBe(false); } /// /// Confirms that a valid configuration with security defaults passes validation. /// [Fact] public void Validator_DefaultSecurityConfig_ReturnsTrue() { var config = LoadFromJson(); ConfigurationValidator.ValidateAndLog(config).ShouldBe(true); } /// /// Confirms that custom security profiles can be bound from in-memory configuration. /// [Fact] public void Security_Section_BindsCustomProfiles() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new[] { new System.Collections.Generic.KeyValuePair("Security:Profiles:0", "None"), new System.Collections.Generic.KeyValuePair("Security:Profiles:1", "Basic256Sha256-SignAndEncrypt"), new System.Collections.Generic.KeyValuePair("Security:AutoAcceptClientCertificates", "false"), new System.Collections.Generic.KeyValuePair("Security:MinimumCertificateKeySize", "4096"), }) .Build(); // Clear default list before binding to match production behavior var config = new AppConfiguration(); config.Security.Profiles.Clear(); configuration.GetSection("Security").Bind(config.Security); config.Security.Profiles.Count.ShouldBe(2); config.Security.Profiles.ShouldContain("None"); config.Security.Profiles.ShouldContain("Basic256Sha256-SignAndEncrypt"); config.Security.AutoAcceptClientCertificates.ShouldBe(false); config.Security.MinimumCertificateKeySize.ShouldBe(4096); } [Fact] public void Redundancy_Section_BindsFromJson() { var config = LoadFromJson(); config.Redundancy.Enabled.ShouldBe(false); config.Redundancy.Mode.ShouldBe("Warm"); config.Redundancy.Role.ShouldBe("Primary"); config.Redundancy.ServiceLevelBase.ShouldBe(200); } [Fact] public void Redundancy_Section_BindsCustomValues() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new[] { new System.Collections.Generic.KeyValuePair("Redundancy:Enabled", "true"), new System.Collections.Generic.KeyValuePair("Redundancy:Mode", "Hot"), new System.Collections.Generic.KeyValuePair("Redundancy:Role", "Secondary"), new System.Collections.Generic.KeyValuePair("Redundancy:ServiceLevelBase", "180"), new System.Collections.Generic.KeyValuePair("Redundancy:ServerUris:0", "urn:a"), new System.Collections.Generic.KeyValuePair("Redundancy:ServerUris:1", "urn:b"), }) .Build(); var config = new AppConfiguration(); configuration.GetSection("Redundancy").Bind(config.Redundancy); config.Redundancy.Enabled.ShouldBe(true); config.Redundancy.Mode.ShouldBe("Hot"); config.Redundancy.Role.ShouldBe("Secondary"); config.Redundancy.ServiceLevelBase.ShouldBe(180); config.Redundancy.ServerUris.Count.ShouldBe(2); } [Fact] public void Validator_RedundancyEnabled_NoApplicationUri_ReturnsFalse() { var config = new AppConfiguration(); config.Redundancy.Enabled = true; config.Redundancy.ServerUris.Add("urn:a"); config.Redundancy.ServerUris.Add("urn:b"); // OpcUa.ApplicationUri is null ConfigurationValidator.ValidateAndLog(config).ShouldBe(false); } [Fact] public void Validator_InvalidServiceLevelBase_ReturnsFalse() { var config = new AppConfiguration(); config.Redundancy.ServiceLevelBase = 0; ConfigurationValidator.ValidateAndLog(config).ShouldBe(false); } [Fact] public void OpcUa_ApplicationUri_DefaultsToNull() { var config = new OpcUaConfiguration(); config.ApplicationUri.ShouldBeNull(); } [Fact] public void OpcUa_ApplicationUri_BindsFromConfig() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new[] { new System.Collections.Generic.KeyValuePair("OpcUa:ApplicationUri", "urn:test:app"), }) .Build(); var config = new OpcUaConfiguration(); configuration.GetSection("OpcUa").Bind(config); config.ApplicationUri.ShouldBe("urn:test:app"); } } }