test(abcip): hw5 review fixes — HardwareGate guard, (host, tag) allow-list, differential-run refusal (#592)
- HardwareGateGuardTests: only StringWriteGateTests may name a write path in HardwareGate/; the allow-list is exactly the two Z-35162 (10.209.70.160) STRING tags; the opt-in, the (host, tag) check and the AB_DIFF_ENDPOINT refusal are present. Proven red by a WriteAsync mutation. - StringWriteGateTests: allow-list entries are (host, tag); any other endpoint is refused; skips while AB_DIFF_ENDPOINT is set, like IntegrationWriteGate. - AbCip.md #566: the close happened somewhere in the 16-60 s window; RST vs FIN not captured. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+29
-8
@@ -15,24 +15,43 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.HardwareGate;
|
||||
/// restored byte-exact).
|
||||
/// <list type="bullet">
|
||||
/// <item><c>AB_HW_WRITE_GATEWAY</c> — <c>ab://host/1,0</c>; absent ⇒ skip.</item>
|
||||
/// <item><c>AB_HW_WRITE_STRING_TAG</c> — must be one of <see cref="Approved"/> (the brief's allow-list).</item>
|
||||
/// <item><c>AB_HW_WRITE_STRING_TAG</c> — with the gateway's host, must be a pair on <see cref="Approved"/>.</item>
|
||||
/// <item><c>AB_HW_WRITE_APPROVED=1</c> — the explicit opt-in.</item>
|
||||
/// <item><c>AB_HW_WRITE_GAP_SECONDS</c> — snapshot gap, default 30, at least 30.</item>
|
||||
/// <item><c>AB_HW_WRITE_DRYRUN=1</c> — stop after the read-only preflight (no write).</item>
|
||||
/// </list>
|
||||
/// Never set by any other run; <see cref="IntegrationWriteGate"/> (the simulator opt-in) is deliberately not
|
||||
/// Refused (skipped) while <c>AB_DIFF_ENDPOINT</c> is set (a read-only differential run). Never set by any other
|
||||
/// run; <see cref="IntegrationWriteGate"/> (the simulator opt-in) is deliberately not
|
||||
/// what enables this.
|
||||
/// </summary>
|
||||
[Trait("Category", "LiveIntegration")]
|
||||
public sealed class StringWriteGateTests
|
||||
{
|
||||
/// <summary>The only tags this gate may write: logic-, alias-, trend- and Galaxy-unreferenced (L5X census + raw grep).</summary>
|
||||
private static readonly string[] Approved =
|
||||
/// <summary>
|
||||
/// The only (controller, tag) pairs this gate may write: Z-35162's logic-, alias-, trend- and Galaxy-unreferenced
|
||||
/// STRINGs (L5X census + raw grep). Pinned to the host, so the same tag name on another controller is refused.
|
||||
/// <see cref="HardwareGateGuardTests"/> pins this list.
|
||||
/// </summary>
|
||||
internal static readonly (string Host, string Tag)[] Approved =
|
||||
[
|
||||
"Program:ES03_TemperatureControl._StrTmp",
|
||||
"Program:ES03_TemperatureControl._Prefix",
|
||||
("10.209.70.160", "Program:ES03_TemperatureControl._StrTmp"),
|
||||
("10.209.70.160", "Program:ES03_TemperatureControl._Prefix"),
|
||||
];
|
||||
|
||||
/// <summary>The host of an <c>ab://host[:port]/path</c> gateway, or null when it is not one.</summary>
|
||||
internal static string? GatewayHost(string gateway)
|
||||
{
|
||||
if (!gateway.StartsWith("ab://", StringComparison.OrdinalIgnoreCase)) return null;
|
||||
var authority = gateway[5..].Split('/', 2)[0];
|
||||
var host = authority.Split(':', 2)[0];
|
||||
return host.Length == 0 ? null : host;
|
||||
}
|
||||
|
||||
/// <summary>True only for an exact (host, tag) pair on <see cref="Approved"/>.</summary>
|
||||
internal static bool IsApproved(string gateway, string tag) =>
|
||||
GatewayHost(gateway) is { } host
|
||||
&& Approved.Any(a => string.Equals(a.Host, host, StringComparison.Ordinal) && string.Equals(a.Tag, tag, StringComparison.Ordinal));
|
||||
|
||||
private const string TestValue = "OTOPCUA-TEST";
|
||||
private const int BuiltInStringBytes = 88;
|
||||
private const int StringCapacity = 82;
|
||||
@@ -42,10 +61,12 @@ public sealed class StringWriteGateTests
|
||||
{
|
||||
var gateway = Environment.GetEnvironmentVariable("AB_HW_WRITE_GATEWAY")?.Trim();
|
||||
if (string.IsNullOrEmpty(gateway)) Assert.Skip("AB_HW_WRITE_GATEWAY not set.");
|
||||
if (Environment.GetEnvironmentVariable(IntegrationWriteGate.DifferentialEndpointVar) is { Length: > 0 })
|
||||
Assert.Skip($"Write gate refused: {IntegrationWriteGate.DifferentialEndpointVar} is set, so a read-only differential run is in progress.");
|
||||
if (Environment.GetEnvironmentVariable("AB_HW_WRITE_APPROVED") != "1") Assert.Skip("AB_HW_WRITE_APPROVED=1 not set.");
|
||||
var tagPath = Environment.GetEnvironmentVariable("AB_HW_WRITE_STRING_TAG")?.Trim() ?? "";
|
||||
if (!Approved.Contains(tagPath, StringComparer.Ordinal))
|
||||
throw new InvalidOperationException($"AB_HW_WRITE_STRING_TAG='{tagPath}' is not on the approved list.");
|
||||
if (!IsApproved(gateway, tagPath))
|
||||
throw new InvalidOperationException($"('{gateway}', '{tagPath}') is not an approved (controller, tag) pair.");
|
||||
var gap = 30;
|
||||
if (Environment.GetEnvironmentVariable("AB_HW_WRITE_GAP_SECONDS") is { Length: > 0 } rawGap
|
||||
&& (!int.TryParse(rawGap, out gap) || gap < 30 || gap > 300))
|
||||
|
||||
Reference in New Issue
Block a user