using System.Net.Http; using System.Net.Sockets; using Grpc.Core; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc; /// /// ScadaBridge#136: admits only connect-phase failures /// (the request provably never left the client) and rejects every shape that may have delivered it. /// public sealed class GrpcRetrySafetyNeverSentTests { private static RpcException Rpc(StatusCode code, string detail, Exception? debug) => new(new Status(code, detail, debug)); private static SocketException Sock(SocketError error) => new((int)error); /// The balancer's subchannel connect failure is pre-send, whatever the socket error. /// The socket error the subchannel connect saw. [Theory] [InlineData(SocketError.ConnectionRefused)] [InlineData(SocketError.HostNotFound)] [InlineData(SocketError.TryAgain)] [InlineData(SocketError.HostUnreachable)] [InlineData(SocketError.TimedOut)] public void SubchannelConnectFailure_IsNeverSent(SocketError error) { var ex = Rpc(StatusCode.Unavailable, GrpcRetrySafety.SubchannelConnectFailureDetail, Sock(error)); Assert.True(GrpcRetrySafety.WasNeverSent(ex)); Assert.True(GrpcRetrySafety.ProvablyNotApplied(ex)); } /// An HttpRequestException connect error with a connect-only socket error is pre-send. /// The HTTP request error classification. /// The socket error. [Theory] [InlineData(HttpRequestError.ConnectionError, SocketError.ConnectionRefused)] [InlineData(HttpRequestError.NameResolutionError, SocketError.HostNotFound)] [InlineData(HttpRequestError.NameResolutionError, SocketError.TryAgain)] public void HttpConnectError_WithConnectOnlySocketError_IsNeverSent(HttpRequestError kind, SocketError error) { var ex = Rpc(StatusCode.Unavailable, "Error starting gRPC call.", new HttpRequestException(kind, "connect failed", Sock(error))); Assert.True(GrpcRetrySafety.WasNeverSent(ex)); } /// Shapes that may follow delivery stay ambiguous. [Fact] public void FailuresThatMayFollowDelivery_AreNotNeverSent() { // Premature end / reset on an established connection. Assert.False(GrpcRetrySafety.WasNeverSent(Rpc(StatusCode.Unavailable, "Error starting gRPC call.", new HttpRequestException(HttpRequestError.ResponseEnded, "ended", new IOException("reset", Sock(SocketError.ConnectionReset)))))); // A ConnectionError classification with a post-connect socket error. Assert.False(GrpcRetrySafety.WasNeverSent(Rpc(StatusCode.Unavailable, "Error starting gRPC call.", new HttpRequestException(HttpRequestError.ConnectionError, "x", Sock(SocketError.ConnectionReset))))); // A server-sent Unavailable (no transport exception at all). Assert.False(GrpcRetrySafety.WasNeverSent(Rpc(StatusCode.Unavailable, "modelled fault", null))); // A bare socket error outside the subchannel-pick path. Assert.False(GrpcRetrySafety.WasNeverSent(Rpc(StatusCode.Unavailable, "Error starting gRPC call.", Sock(SocketError.ConnectionRefused)))); // Subchannel detail without a socket cause. Assert.False(GrpcRetrySafety.WasNeverSent(Rpc(StatusCode.Unavailable, GrpcRetrySafety.SubchannelConnectFailureDetail, new InvalidOperationException()))); // Not Unavailable. Assert.False(GrpcRetrySafety.WasNeverSent(Rpc(StatusCode.DeadlineExceeded, GrpcRetrySafety.SubchannelConnectFailureDetail, Sock(SocketError.ConnectionRefused)))); } }