Adds Security section to appsettings.json with configurable OPC UA transport profiles (None, Basic256Sha256-Sign, Basic256Sha256-SignAndEncrypt), certificate policy settings, and a configurable BindAddress for the OPC UA endpoint. Defaults preserve backward compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
241 lines
9.7 KiB
C#
241 lines
9.7 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
|
|
|
namespace ZB.MOM.WW.LmxOpcUa.Tests.Configuration
|
|
{
|
|
/// <summary>
|
|
/// Verifies that application configuration binds correctly from appsettings and that validation catches invalid bridge settings.
|
|
/// </summary>
|
|
public class ConfigurationLoadingTests
|
|
{
|
|
/// <summary>
|
|
/// Loads the application configuration from the repository appsettings file for binding tests.
|
|
/// </summary>
|
|
/// <returns>The bound application configuration snapshot.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the OPC UA section binds the endpoint and session settings expected by the bridge.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the MXAccess section binds runtime timeout and reconnect settings correctly.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the Galaxy repository section binds connection and polling settings correctly.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that extended-attribute loading defaults to disabled when not configured.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GalaxyRepository_ExtendedAttributes_DefaultsFalse()
|
|
{
|
|
var config = new GalaxyRepositoryConfiguration();
|
|
config.ExtendedAttributes.ShouldBe(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the extended-attribute flag can be enabled through configuration binding.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GalaxyRepository_ExtendedAttributes_BindsFromJson()
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddJsonFile("appsettings.json", optional: false)
|
|
.AddInMemoryCollection(new[] { new System.Collections.Generic.KeyValuePair<string, string>("GalaxyRepository:ExtendedAttributes", "true") })
|
|
.Build();
|
|
|
|
var config = new GalaxyRepositoryConfiguration();
|
|
configuration.GetSection("GalaxyRepository").Bind(config);
|
|
config.ExtendedAttributes.ShouldBe(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the dashboard section binds operator-dashboard settings correctly.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Dashboard_Section_BindsCorrectly()
|
|
{
|
|
var config = LoadFromJson();
|
|
config.Dashboard.Enabled.ShouldBe(true);
|
|
config.Dashboard.Port.ShouldBe(8081);
|
|
config.Dashboard.RefreshIntervalSeconds.ShouldBe(10);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the default configuration objects start with the expected bridge defaults.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that BindAddress can be overridden to a specific hostname or IP.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OpcUa_BindAddress_CanBeOverridden()
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new[]
|
|
{
|
|
new System.Collections.Generic.KeyValuePair<string, string>("OpcUa:BindAddress", "localhost"),
|
|
})
|
|
.Build();
|
|
|
|
var config = new OpcUaConfiguration();
|
|
configuration.GetSection("OpcUa").Bind(config);
|
|
config.BindAddress.ShouldBe("localhost");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that a valid configuration passes startup validation.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Validator_ValidConfig_ReturnsTrue()
|
|
{
|
|
var config = LoadFromJson();
|
|
ConfigurationValidator.ValidateAndLog(config).ShouldBe(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that an invalid OPC UA port is rejected by startup validation.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Validator_InvalidPort_ReturnsFalse()
|
|
{
|
|
var config = new AppConfiguration();
|
|
config.OpcUa.Port = 0;
|
|
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that an empty Galaxy name is rejected because the bridge requires a namespace target.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Validator_EmptyGalaxyName_ReturnsFalse()
|
|
{
|
|
var config = new AppConfiguration();
|
|
config.OpcUa.GalaxyName = "";
|
|
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that the Security section binds profile list from appsettings.json.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Security_Section_BindsProfilesCorrectly()
|
|
{
|
|
var config = LoadFromJson();
|
|
config.Security.Profiles.ShouldContain("None");
|
|
config.Security.AutoAcceptClientCertificates.ShouldBe(true);
|
|
config.Security.MinimumCertificateKeySize.ShouldBe(2048);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that a minimum key size below 2048 is rejected by the validator.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Validator_InvalidMinKeySize_ReturnsFalse()
|
|
{
|
|
var config = new AppConfiguration();
|
|
config.Security.MinimumCertificateKeySize = 1024;
|
|
ConfigurationValidator.ValidateAndLog(config).ShouldBe(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that a valid configuration with security defaults passes validation.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Validator_DefaultSecurityConfig_ReturnsTrue()
|
|
{
|
|
var config = LoadFromJson();
|
|
ConfigurationValidator.ValidateAndLog(config).ShouldBe(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Confirms that custom security profiles can be bound from in-memory configuration.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Security_Section_BindsCustomProfiles()
|
|
{
|
|
var configuration = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new[]
|
|
{
|
|
new System.Collections.Generic.KeyValuePair<string, string>("Security:Profiles:0", "None"),
|
|
new System.Collections.Generic.KeyValuePair<string, string>("Security:Profiles:1", "Basic256Sha256-SignAndEncrypt"),
|
|
new System.Collections.Generic.KeyValuePair<string, string>("Security:AutoAcceptClientCertificates", "false"),
|
|
new System.Collections.Generic.KeyValuePair<string, string>("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);
|
|
}
|
|
}
|
|
}
|