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))));
}
}