PR 5.4 — Write-by-classification parity scenarios
Both backends route a write through the same path keyed off the attribute's SecurityClassification, so a single write request must produce the same StatusCode on each: - FreeAccess_or_Operate_write_returns_same_StatusCode_on_both_backends picks the first numeric FreeAccess/Operate attribute and writes 0.0. - Configure_class_write_routes_through_secured_path_on_both_backends picks a Configure/Tune attribute, writes through the secured path, asserts StatusCode parity (the test doesn't care whether the write succeeds — only that both backends produce the same outcome). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PR 5.4 — Write-by-classification parity. Each driver routes writes by the
|
||||||
|
/// attribute's <see cref="SecurityClassification"/>: <c>FreeAccess</c> /
|
||||||
|
/// <c>Operate</c> use plain <c>Write</c>; <c>Tune</c> / <c>Configure</c> /
|
||||||
|
/// <c>VerifiedWrite</c> use <c>WriteSecured</c>. Both backends must surface the
|
||||||
|
/// same StatusCode for the same write request — successful for FreeAccess /
|
||||||
|
/// Operate (assuming the dev Galaxy has at least one writable attribute) and
|
||||||
|
/// failure for Configure when no auth principal is supplied.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "ParityE2E")]
|
||||||
|
[Collection(nameof(ParityCollection))]
|
||||||
|
public sealed class WriteByClassificationParityTests
|
||||||
|
{
|
||||||
|
private readonly ParityHarness _h;
|
||||||
|
public WriteByClassificationParityTests(ParityHarness h) => _h = h;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FreeAccess_or_Operate_write_returns_same_StatusCode_on_both_backends()
|
||||||
|
{
|
||||||
|
_h.RequireBoth();
|
||||||
|
|
||||||
|
var b = new RecordingAddressSpaceBuilder();
|
||||||
|
await ((ITagDiscovery)_h.LegacyDriver!).DiscoverAsync(b, CancellationToken.None);
|
||||||
|
|
||||||
|
var target = b.Variables.FirstOrDefault(v =>
|
||||||
|
v.AttributeInfo.SecurityClass is SecurityClassification.FreeAccess or SecurityClassification.Operate
|
||||||
|
&& v.AttributeInfo.DriverDataType is DriverDataType.Float32 or DriverDataType.Float64 or DriverDataType.Int32);
|
||||||
|
if (target is null) Assert.Skip("no FreeAccess/Operate numeric writable attribute on dev Galaxy");
|
||||||
|
|
||||||
|
var request = new[] { new WriteRequest(target.AttributeInfo.FullName, 0.0) };
|
||||||
|
var results = await _h.RunOnAvailableAsync(
|
||||||
|
(driver, ct) => ((IWritable)driver).WriteAsync(request, ct),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
results[ParityHarness.Backend.LegacyHost][0].StatusCode
|
||||||
|
.ShouldBe(results[ParityHarness.Backend.MxGateway][0].StatusCode,
|
||||||
|
$"FreeAccess/Operate StatusCode parity for '{target.AttributeInfo.FullName}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Configure_class_write_routes_through_secured_path_on_both_backends()
|
||||||
|
{
|
||||||
|
_h.RequireBoth();
|
||||||
|
|
||||||
|
var b = new RecordingAddressSpaceBuilder();
|
||||||
|
await ((ITagDiscovery)_h.LegacyDriver!).DiscoverAsync(b, CancellationToken.None);
|
||||||
|
|
||||||
|
var target = b.Variables.FirstOrDefault(v =>
|
||||||
|
v.AttributeInfo.SecurityClass is SecurityClassification.Configure or SecurityClassification.Tune);
|
||||||
|
if (target is null) Assert.Skip("no Configure/Tune attribute on dev Galaxy");
|
||||||
|
|
||||||
|
var request = new[] { new WriteRequest(target.AttributeInfo.FullName, 0.0) };
|
||||||
|
var results = await _h.RunOnAvailableAsync(
|
||||||
|
(driver, ct) => ((IWritable)driver).WriteAsync(request, ct),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
// Both backends route through the secured-write path. The exact StatusCode
|
||||||
|
// depends on whether the running test identity has write permission on the
|
||||||
|
// dev Galaxy — what matters here is that they agree, not which value they
|
||||||
|
// produce. (Parity, not policy.)
|
||||||
|
results[ParityHarness.Backend.LegacyHost][0].StatusCode
|
||||||
|
.ShouldBe(results[ParityHarness.Backend.MxGateway][0].StatusCode,
|
||||||
|
$"Secured-write StatusCode parity for '{target.AttributeInfo.FullName}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user