using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
///
/// Task 22 review r1: a runtime that refuses an operation with (the native
/// transport does this before the wire, e.g. a write it cannot type) reaches the client as BadNotSupported —
/// NOT as a comms fault. The handle is kept (one create, never disposed) and driver health is untouched.
///
[Trait("Category", "Unit")]
public sealed class AbCipLocalRefusalTests
{
private const string Host = "ab://10.0.0.5/1,0";
private sealed class RefusingTag(AbCipTagCreateParams p, bool refuseRead, bool refuseWrite) : FakeAbCipTag(p)
{
public override Task ReadAsync(CancellationToken cancellationToken) =>
refuseRead
? Task.FromException(new AbTransportException(AbStatus.Unsupported, "refused read"))
: base.ReadAsync(cancellationToken);
public override Task WriteAsync(CancellationToken cancellationToken) =>
refuseWrite
? Task.FromException(new AbTransportException(AbStatus.Unsupported, "refused write"))
: base.WriteAsync(cancellationToken);
}
private static async Task<(AbCipDriver Driver, FakeAbCipTagFactory Factory, Func Creates)> NewAsync(
bool refuseRead, bool refuseWrite, AbCipDataType type = AbCipDataType.DInt)
{
var creates = 0;
var factory = new FakeAbCipTagFactory
{
Customise = p => { Interlocked.Increment(ref creates); return new RefusingTag(p, refuseRead, refuseWrite) { Value = 1 }; },
};
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Host)],
RawTags = AbCipRawTags.From(new AbCipTagDefinition("T", Host, "T", type)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
return (drv, factory, () => Volatile.Read(ref creates));
}
#pragma warning disable OTOPCUA0001 // the driver's own status mapping is under test, not resilience wrapping
[Fact]
public async Task A_refused_write_is_BadNotSupported_keeps_the_handle_and_leaves_health_alone()
{
var (drv, factory, creates) = await NewAsync(refuseRead: false, refuseWrite: true);
(await drv.ReadAsync(["T"], CancellationToken.None)).Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
var health = drv.GetHealth();
(await drv.WriteAsync([new WriteRequest("T", 5)], CancellationToken.None)).Single().StatusCode
.ShouldBe(AbCipStatusMapper.BadNotSupported);
drv.GetHealth().ShouldBe(health);
factory.Tags["T"].Disposed.ShouldBeFalse();
(await drv.ReadAsync(["T"], CancellationToken.None)).Single().StatusCode.ShouldBe(AbCipStatusMapper.Good);
creates().ShouldBe(1);
}
[Fact]
public async Task A_refused_read_is_BadNotSupported_keeps_the_handle_and_leaves_health_alone()
{
var (drv, factory, creates) = await NewAsync(refuseRead: true, refuseWrite: false);
var health = drv.GetHealth();
for (var n = 0; n < 2; n++)
(await drv.ReadAsync(["T"], CancellationToken.None)).Single().StatusCode
.ShouldBe(AbCipStatusMapper.BadNotSupported);
drv.GetHealth().ShouldBe(health);
factory.Tags["T"].Disposed.ShouldBeFalse();
creates().ShouldBe(1);
}
/// Control: any OTHER transport failure is still a comms fault (evicted, Degraded).
[Fact]
public async Task A_non_refusal_write_failure_still_evicts_and_degrades()
{
var factory = new FakeAbCipTagFactory();
factory.Customise = p => new ThrowingWriteTag(p) { Value = 1 };
var drv = new AbCipDriver(new AbCipDriverOptions
{
Devices = [new AbCipDeviceOptions(Host)],
RawTags = AbCipRawTags.From(new AbCipTagDefinition("T", Host, "T", AbCipDataType.DInt)),
Probe = new AbCipProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
(await drv.WriteAsync([new WriteRequest("T", 5)], CancellationToken.None)).Single().StatusCode
.ShouldBe(AbCipStatusMapper.BadCommunicationError);
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
factory.Tags["T"].Disposed.ShouldBeTrue();
}
#pragma warning restore OTOPCUA0001
private sealed class ThrowingWriteTag(AbCipTagCreateParams p) : FakeAbCipTag(p)
{
public override Task WriteAsync(CancellationToken cancellationToken) =>
Task.FromException(new AbTransportException(AbStatus.BadConnection, "socket closed"));
}
}