using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests; /// /// PR 5.4 — Write-by-classification parity. Each driver routes writes by the /// attribute's : FreeAccess / /// Operate use plain Write; Tune / Configure / /// VerifiedWrite use WriteSecured. 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. /// [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}'"); } }