fix(r2-06): RED — ServerHistorianOptionsValidator unit + wiring tests (net-new type; does not compile) (06/S-11)
This commit is contained in:
@@ -4,13 +4,13 @@
|
|||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"id": "T1",
|
"id": "T1",
|
||||||
"subject": "RED: misconfig repro test \u2014 HistorianGatewayClientAdapter.Create with empty/malformed Endpoint must throw a named InvalidOperationException (currently UriFormatException; test MUST fail on current code)",
|
"subject": "RED: misconfig repro test — HistorianGatewayClientAdapter.Create with empty/malformed Endpoint must throw a named InvalidOperationException (currently UriFormatException; test MUST fail on current code)",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"blockedBy": []
|
"blockedBy": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T2",
|
"id": "T2",
|
||||||
"subject": "GREEN: adapter guard \u2014 Uri.TryCreate + InvalidOperationException naming ServerHistorian:Endpoint in HistorianGatewayClientAdapter.Create (defense in depth)",
|
"subject": "GREEN: adapter guard — Uri.TryCreate + InvalidOperationException naming ServerHistorian:Endpoint in HistorianGatewayClientAdapter.Create (defense in depth)",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"blockedBy": [
|
"blockedBy": [
|
||||||
"T1"
|
"T1"
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
{
|
{
|
||||||
"id": "T3",
|
"id": "T3",
|
||||||
"subject": "RED: ServerHistorianOptionsValidator unit tests (7 cases incl. the AlarmHistorian-enabled/ServerHistorian-disabled corner naming both sections)",
|
"subject": "RED: ServerHistorianOptionsValidator unit tests (7 cases incl. the AlarmHistorian-enabled/ServerHistorian-disabled corner naming both sections)",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": [
|
"blockedBy": [
|
||||||
"T1"
|
"T1"
|
||||||
]
|
]
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T7",
|
"id": "T7",
|
||||||
"subject": "U-7 pin: GatewayTagProvisionerTests Boolean_definition_carries_explicit_Int1_presence (asserts HistorianDataType.Int1 AND HasDataType \u2014 0.2.0 proto3-optional presence witness; pin, no RED phase)",
|
"subject": "U-7 pin: GatewayTagProvisionerTests Boolean_definition_carries_explicit_Int1_presence (asserts HistorianDataType.Int1 AND HasDataType — 0.2.0 proto3-optional presence witness; pin, no RED phase)",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": []
|
"blockedBy": []
|
||||||
},
|
},
|
||||||
|
|||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// archreview 06/S-11 — verifies the net-new <see cref="ServerHistorianOptionsValidator"/> (built on
|
||||||
|
/// the shared <c>ZB.MOM.WW.Configuration</c> <c>OptionsValidatorBase</c>/<c>ValidationBuilder</c>).
|
||||||
|
/// The fail tier is deliberately narrow: only <b>provably-crashing</b> configs fail — an enabled
|
||||||
|
/// historian (or an enabled <c>AlarmHistorian</c>, which sources its gateway connection from the
|
||||||
|
/// <c>ServerHistorian</c> section) with an empty / non-absolute / non-http(s) <c>Endpoint</c>, i.e.
|
||||||
|
/// exactly the configs that would otherwise throw <see cref="System.UriFormatException"/> deep in the
|
||||||
|
/// gateway client factory at startup. Empty <c>ApiKey</c> / non-positive <c>MaxTieClusterOverfetch</c>
|
||||||
|
/// stay operator warnings in <see cref="ServerHistorianOptions.Validate"/> (they degrade, not crash).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ServerHistorianOptionsValidatorTests
|
||||||
|
{
|
||||||
|
private static ServerHistorianOptionsValidator Sut(bool alarmHistorianEnabled = false)
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["AlarmHistorian:Enabled"] = alarmHistorianEnabled ? "true" : "false",
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
return new ServerHistorianOptionsValidator(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enabled historian with an empty endpoint fails (would crash-loop in the factory).</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Enabled_with_empty_endpoint_fails()
|
||||||
|
{
|
||||||
|
var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "" });
|
||||||
|
|
||||||
|
result.Failed.ShouldBeTrue();
|
||||||
|
result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enabled with a relative (non-absolute) URI fails.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Enabled_with_relative_uri_fails()
|
||||||
|
{
|
||||||
|
var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "host:5222" });
|
||||||
|
|
||||||
|
result.Failed.ShouldBeTrue();
|
||||||
|
result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enabled with a non-http(s) scheme (ftp) fails.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Enabled_with_bad_scheme_fails()
|
||||||
|
{
|
||||||
|
var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "ftp://host:5222" });
|
||||||
|
|
||||||
|
result.Failed.ShouldBeTrue();
|
||||||
|
result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enabled with a valid absolute https endpoint succeeds.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Enabled_with_valid_https_endpoint_succeeds()
|
||||||
|
{
|
||||||
|
var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "https://host:5222" });
|
||||||
|
|
||||||
|
result.Succeeded.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A disabled section may legitimately be empty — no failure.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Disabled_with_empty_endpoint_succeeds()
|
||||||
|
{
|
||||||
|
var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = false, Endpoint = "" });
|
||||||
|
|
||||||
|
result.Succeeded.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The residual corner: <c>ServerHistorian:Enabled=false</c> but <c>AlarmHistorian:Enabled=true</c>
|
||||||
|
/// (which sources its connection from the ServerHistorian section) with an empty endpoint fails —
|
||||||
|
/// and the message names BOTH sections so the operator understands why a disabled section is required.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Disabled_but_alarm_historian_enabled_with_empty_endpoint_fails_naming_both_sections()
|
||||||
|
{
|
||||||
|
var result = Sut(alarmHistorianEnabled: true)
|
||||||
|
.Validate(null, new ServerHistorianOptions { Enabled = false, Endpoint = "" });
|
||||||
|
|
||||||
|
result.Failed.ShouldBeTrue();
|
||||||
|
result.Failures.ShouldContain(f =>
|
||||||
|
f.Contains("ServerHistorian:Endpoint") && f.Contains("AlarmHistorian:Enabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Alarm-only mode with a valid endpoint succeeds.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Disabled_but_alarm_historian_enabled_with_valid_endpoint_succeeds()
|
||||||
|
{
|
||||||
|
var result = Sut(alarmHistorianEnabled: true)
|
||||||
|
.Validate(null, new ServerHistorianOptions { Enabled = false, Endpoint = "https://host:5222" });
|
||||||
|
|
||||||
|
result.Succeeded.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wiring guard (the "register-AND-consume" trap): resolving <c>IOptions<ServerHistorianOptions>.Value</c>
|
||||||
|
/// after <see cref="ServiceCollectionExtensions.AddValidatedOptions{TOptions,TValidator}"/> throws
|
||||||
|
/// <see cref="OptionsValidationException"/> for an enabled+empty section — proving the validator is
|
||||||
|
/// attached to the options pipeline (not merely defined). <c>ValidateOnStart</c> reaching host start
|
||||||
|
/// is already proven by the LDAP precedent.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void AddValidatedOptions_wiring_throws_on_enabled_empty_endpoint()
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["ServerHistorian:Enabled"] = "true",
|
||||||
|
["ServerHistorian:Endpoint"] = "",
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<IConfiguration>(configuration);
|
||||||
|
services.AddValidatedOptions<ServerHistorianOptions, ServerHistorianOptionsValidator>(
|
||||||
|
configuration, ServerHistorianOptions.SectionName);
|
||||||
|
using var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var ex = Should.Throw<OptionsValidationException>(
|
||||||
|
() => _ = provider.GetRequiredService<IOptions<ServerHistorianOptions>>().Value);
|
||||||
|
ex.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user