fix(r2-06): GREEN adapter guard — TryCreate + named InvalidOperationException on bad ServerHistorian:Endpoint (06/S-11)
This commit is contained in:
@@ -4,43 +4,53 @@
|
|||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"id": "T1",
|
"id": "T1",
|
||||||
"subject": "RED: misconfig repro test — HistorianGatewayClientAdapter.Create with empty/malformed Endpoint must throw a named InvalidOperationException (currently UriFormatException; test MUST fail on current code)",
|
"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)",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"blockedBy": []
|
"blockedBy": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T2",
|
"id": "T2",
|
||||||
"subject": "GREEN: adapter guard — Uri.TryCreate + InvalidOperationException naming ServerHistorian:Endpoint in HistorianGatewayClientAdapter.Create (defense in depth)",
|
"subject": "GREEN: adapter guard \u2014 Uri.TryCreate + InvalidOperationException naming ServerHistorian:Endpoint in HistorianGatewayClientAdapter.Create (defense in depth)",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": ["T1"]
|
"blockedBy": [
|
||||||
|
"T1"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": "pending",
|
||||||
"blockedBy": ["T1"]
|
"blockedBy": [
|
||||||
|
"T1"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T4",
|
"id": "T4",
|
||||||
"subject": "GREEN: implement ServerHistorianOptionsValidator (OptionsValidatorBase, ctor IConfiguration, consumed = Enabled || AlarmHistorian:Enabled, fail on empty/non-absolute/non-http(s) Endpoint)",
|
"subject": "GREEN: implement ServerHistorianOptionsValidator (OptionsValidatorBase, ctor IConfiguration, consumed = Enabled || AlarmHistorian:Enabled, fail on empty/non-absolute/non-http(s) Endpoint)",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": ["T3"]
|
"blockedBy": [
|
||||||
|
"T3"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T5",
|
"id": "T5",
|
||||||
"subject": "Wiring: AddValidatedOptions<ServerHistorianOptions, ServerHistorianOptionsValidator> in Program.cs hasDriver block + OptionsValidationException wiring test + residual alarm-only-mode startup warning",
|
"subject": "Wiring: AddValidatedOptions<ServerHistorianOptions, ServerHistorianOptionsValidator> in Program.cs hasDriver block + OptionsValidationException wiring test + residual alarm-only-mode startup warning",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": ["T4"]
|
"blockedBy": [
|
||||||
|
"T4"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T6",
|
"id": "T6",
|
||||||
"subject": "Warning parity: malformed-endpoint warning in ServerHistorianOptions.Validate() + Runtime.Tests case; commit 1 (S-11) after T1-T6 green",
|
"subject": "Warning parity: malformed-endpoint warning in ServerHistorianOptions.Validate() + Runtime.Tests case; commit 1 (S-11) after T1-T6 green",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": ["T4"]
|
"blockedBy": [
|
||||||
|
"T4"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T7",
|
"id": "T7",
|
||||||
"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)",
|
"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)",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": []
|
"blockedBy": []
|
||||||
},
|
},
|
||||||
@@ -48,13 +58,17 @@
|
|||||||
"id": "T8",
|
"id": "T8",
|
||||||
"subject": "U-7 live leg: HISTGW_BOOL_SANDBOX_TAG in GatewayLiveFixture + EnsureTags_boolean_sandbox_provisions_as_Int1 + EnsureTags_type_change_outcome_is_observable retype probe (skip-clean offline; verify all-skip run)",
|
"subject": "U-7 live leg: HISTGW_BOOL_SANDBOX_TAG in GatewayLiveFixture + EnsureTags_boolean_sandbox_provisions_as_Int1 + EnsureTags_type_change_outcome_is_observable retype probe (skip-clean offline; verify all-skip run)",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": ["T7"]
|
"blockedBy": [
|
||||||
|
"T7"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T9",
|
"id": "T9",
|
||||||
"subject": "U-7 docs: migration/assessment note (EnsureTags is upsert; pre-bump Float Booleans; three retype hypotheses; failed=N tally observability) in docs/Historian.md + CLAUDE.md HISTGW_BOOL_SANDBOX_TAG recipe update; commit 2 (U-7) after T7-T9",
|
"subject": "U-7 docs: migration/assessment note (EnsureTags is upsert; pre-bump Float Booleans; three retype hypotheses; failed=N tally observability) in docs/Historian.md + CLAUDE.md HISTGW_BOOL_SANDBOX_TAG recipe update; commit 2 (U-7) after T7-T9",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": ["T8"]
|
"blockedBy": [
|
||||||
|
"T8"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T10",
|
"id": "T10",
|
||||||
@@ -66,13 +80,20 @@
|
|||||||
"id": "T11",
|
"id": "T11",
|
||||||
"subject": "Bookkeeping: update archreview/plans/STATUS.md + 06-gateway-integrations.md prior-finding rows for S-11/U-7/C-7 when the work lands",
|
"subject": "Bookkeeping: update archreview/plans/STATUS.md + 06-gateway-integrations.md prior-finding rows for S-11/U-7/C-7 when the work lands",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": ["T6", "T9", "T10"]
|
"blockedBy": [
|
||||||
|
"T6",
|
||||||
|
"T9",
|
||||||
|
"T10"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "T12",
|
"id": "T12",
|
||||||
"subject": "OPERATOR GATE: run Category=LiveIntegration on the VPN against a 0.2.0 gateway (all 6 tests, incl. Boolean EnsureTags + retype probe); record the observed retype policy back into docs/Historian.md + STATUS.md; also discharges 06/U-2's full documented run",
|
"subject": "OPERATOR GATE: run Category=LiveIntegration on the VPN against a 0.2.0 gateway (all 6 tests, incl. Boolean EnsureTags + retype probe); record the observed retype policy back into docs/Historian.md + STATUS.md; also discharges 06/U-2's full documented run",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
"blockedBy": ["T9", "T11"]
|
"blockedBy": [
|
||||||
|
"T9",
|
||||||
|
"T11"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -35,14 +35,31 @@ public sealed class HistorianGatewayClientAdapter : IHistorianGatewayClient, IDi
|
|||||||
/// <param name="options">The bound <c>ServerHistorian</c> configuration (endpoint, key, TLS posture).</param>
|
/// <param name="options">The bound <c>ServerHistorian</c> configuration (endpoint, key, TLS posture).</param>
|
||||||
/// <param name="loggerFactory">Logger factory threaded into the package client's channel diagnostics.</param>
|
/// <param name="loggerFactory">Logger factory threaded into the package client's channel diagnostics.</param>
|
||||||
/// <returns>A ready-to-use adapter whose underlying channel has not yet dialed the gateway.</returns>
|
/// <returns>A ready-to-use adapter whose underlying channel has not yet dialed the gateway.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// <see cref="ServerHistorianOptions.Endpoint"/> is empty or not an absolute <c>http(s)</c> URI.
|
||||||
|
/// Defense in depth (archreview 06/S-11): the Host's <c>ServerHistorianOptionsValidator</c> fails
|
||||||
|
/// these configs at startup, but any caller that bypasses <c>ValidateOnStart</c> (tests, the live
|
||||||
|
/// fixture, future non-Host wiring) still gets a named, config-key-carrying error here instead of
|
||||||
|
/// the raw <see cref="UriFormatException"/> the bare <c>new Uri(...)</c> ctor throws.
|
||||||
|
/// </exception>
|
||||||
public static HistorianGatewayClientAdapter Create(ServerHistorianOptions options, ILoggerFactory loggerFactory)
|
public static HistorianGatewayClientAdapter Create(ServerHistorianOptions options, ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
ArgumentNullException.ThrowIfNull(loggerFactory);
|
ArgumentNullException.ThrowIfNull(loggerFactory);
|
||||||
|
|
||||||
|
// Fail fast with a named, actionable error rather than letting `new Uri("")` throw a raw
|
||||||
|
// UriFormatException deep in the factory (archreview 06/S-11). The Endpoint is not a secret,
|
||||||
|
// so it is safe to echo; the ApiKey is never surfaced.
|
||||||
|
if (!Uri.TryCreate(options.Endpoint, UriKind.Absolute, out var endpointUri)
|
||||||
|
|| (endpointUri.Scheme != Uri.UriSchemeHttp && endpointUri.Scheme != Uri.UriSchemeHttps))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"ServerHistorian:Endpoint must be an absolute http(s) URI (e.g. https://host:5222); got '{options.Endpoint}'.");
|
||||||
|
}
|
||||||
|
|
||||||
var clientOptions = new HistorianGatewayClientOptions
|
var clientOptions = new HistorianGatewayClientOptions
|
||||||
{
|
{
|
||||||
Endpoint = new Uri(options.Endpoint),
|
Endpoint = endpointUri,
|
||||||
ApiKey = options.ApiKey,
|
ApiKey = options.ApiKey,
|
||||||
UseTls = options.UseTls,
|
UseTls = options.UseTls,
|
||||||
CaCertificatePath = options.CaCertificatePath,
|
CaCertificatePath = options.CaCertificatePath,
|
||||||
|
|||||||
Reference in New Issue
Block a user