Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/HardwareGate/HardwareGateGuardTests.cs
T
Joseph Doherty c36224d956 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
2026-10-09 06:21:55 -04:00

71 lines
3.8 KiB
C#

using System.Runtime.CompilerServices;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.HardwareGate;
/// <summary>
/// Offline guard for the hardware-gate folder — deliberately NOT env-gated, so it runs in every build. The folder sits
/// outside the read-only <c>Differential</c> guard because one test in it writes; this pins how far that goes:
/// only <see cref="StringWriteGateTests"/> may name a write path, its allow-list is exactly the two approved
/// (Z-35162, tag) pairs, and its explicit opt-in and differential-run refusal are present.
/// </summary>
public sealed class HardwareGateGuardTests
{
[Fact]
public void Only_the_string_write_gate_names_a_write_path()
{
// Built by concatenation so this file's own text never matches itself.
string[] forbidden =
[
"Write" + "Async", "Encode" + "Value", "WriteBit" + "InDInt", "Write" + "Tag", "Write" + "Request", "AbCip" + "Driver",
];
var sources = Directory.GetFiles(Path.GetDirectoryName(ThisFile())!, "*.cs");
sources.ShouldContain(f => Path.GetFileName(f) == nameof(StringWriteGateTests) + ".cs");
sources.ShouldContain(f => Path.GetFileName(f) == nameof(IdleConnectionEvidenceTests) + ".cs");
var hits = (from f in sources
where Path.GetFileName(f) != nameof(StringWriteGateTests) + ".cs"
let text = File.ReadAllText(f)
from token in forbidden
where text.Contains(token, StringComparison.Ordinal)
select $"{Path.GetFileName(f)} names {token}").ToList();
hits.ShouldBeEmpty();
// Positive control: the scan does see the one file that is allowed to write.
var writer = File.ReadAllText(sources.Single(f => Path.GetFileName(f) == nameof(StringWriteGateTests) + ".cs"));
writer.ShouldContain(forbidden[0]);
}
[Fact]
public void The_write_allow_list_is_exactly_the_two_approved_pairs_on_Z35162()
{
StringWriteGateTests.Approved.ShouldBe(
[
("10.209.70.160", "Program:ES03_TemperatureControl._StrTmp"),
("10.209.70.160", "Program:ES03_TemperatureControl._Prefix"),
]);
StringWriteGateTests.IsApproved("ab://10.209.70.160/1,0", "Program:ES03_TemperatureControl._StrTmp").ShouldBeTrue();
StringWriteGateTests.IsApproved("ab://10.209.70.160:44818/1,0", "Program:ES03_TemperatureControl._Prefix").ShouldBeTrue();
// Same tag on any other controller, another tag on Z-35162, or a malformed gateway: refused.
StringWriteGateTests.IsApproved("ab://10.209.70.161/1,0", "Program:ES03_TemperatureControl._StrTmp").ShouldBeFalse();
StringWriteGateTests.IsApproved("ab://10.100.0.35/1,0", "Program:ES03_TemperatureControl._StrTmp").ShouldBeFalse();
StringWriteGateTests.IsApproved("ab://10.209.70.160/1,0", "test2").ShouldBeFalse();
StringWriteGateTests.IsApproved("ab://10.209.70.160/1,0", "Program:UN01_Press._Prefix").ShouldBeFalse();
StringWriteGateTests.IsApproved("10.209.70.160", "Program:ES03_TemperatureControl._StrTmp").ShouldBeFalse();
}
[Fact]
public void The_string_write_gate_keeps_its_opt_in_and_refuses_during_a_differential_run()
{
var text = File.ReadAllText(Path.Combine(Path.GetDirectoryName(ThisFile())!, nameof(StringWriteGateTests) + ".cs"));
text.ShouldContain("""GetEnvironmentVariable("AB_HW_WRITE_APPROVED") != "1") Assert.Skip(""");
text.ShouldContain("GetEnvironmentVariable(IntegrationWriteGate.DifferentialEndpointVar) is { Length: > 0 })");
text.ShouldContain("if (!IsApproved(gateway, tagPath))");
IntegrationWriteGate.DifferentialEndpointVar.ShouldBe("AB_DIFF_ENDPOINT");
}
private static string ThisFile([CallerFilePath] string path = "") => path;
}