using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Opc.Ua;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests;
///
/// T17: Unit tests for — the pure
/// (exception + captured cert) → mapping that backs the
/// OPC UA verify-endpoint probe. These exercise every
/// branch WITHOUT a live OPC UA server, so the classification logic is covered
/// deterministically.
///
public class VerifyEndpointResultMappingTests
{
///
/// Builds a throwaway self-signed certificate so the untrusted-cert branch can be
/// exercised with a real (and real DER bytes) without a
/// live server. Not persisted; only its public fields are asserted.
///
private static X509Certificate2 BuildSelfSignedCert(
string subjectName = "CN=ScadaBridge-Verify-Test")
{
using var rsa = RSA.Create(2048);
var request = new CertificateRequest(
subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var notBefore = DateTimeOffset.UtcNow.AddMinutes(-5);
var notAfter = DateTimeOffset.UtcNow.AddDays(1);
return request.CreateSelfSigned(notBefore, notAfter);
}
[Fact]
public void CapturedCert_WithValidationException_MapsToUntrustedCertificate()
{
using var cert = BuildSelfSignedCert();
// A certificate-validation rejection surfaces as a ServiceResultException with
// BadCertificateUntrusted; the captured cert is what makes the kind authoritative.
var ex = new ServiceResultException(
StatusCodes.BadCertificateUntrusted, "Certificate is not trusted.");
var result = RealOpcUaClient.MapVerifyOutcome(ex, cert);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.UntrustedCertificate, result.FailureKind);
Assert.NotNull(result.Cert);
Assert.Equal(cert.Thumbprint, result.Cert!.Thumbprint);
Assert.Equal(cert.Subject, result.Cert.Subject);
Assert.Equal(cert.Issuer, result.Cert.Issuer);
// The captured DER must round-trip to the original raw bytes verbatim.
Assert.Equal(Convert.ToBase64String(cert.RawData), result.Cert.DerBase64);
Assert.Equal(DateTimeKind.Utc, result.Cert.NotBeforeUtc.Kind);
Assert.Equal(DateTimeKind.Utc, result.Cert.NotAfterUtc.Kind);
Assert.NotNull(result.Error);
}
[Fact]
public void CapturedCert_WithoutException_MapsToUntrustedCertificate()
{
// The validator may capture+reject the cert and the SDK then fail with a different
// (or no) exception. A captured cert ALWAYS dominates the classification.
using var cert = BuildSelfSignedCert();
var result = RealOpcUaClient.MapVerifyOutcome(null, cert);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.UntrustedCertificate, result.FailureKind);
Assert.NotNull(result.Cert);
Assert.Equal(cert.Thumbprint, result.Cert!.Thumbprint);
}
[Fact]
public void TimeoutException_MapsToTimeout()
{
var result = RealOpcUaClient.MapVerifyOutcome(new TimeoutException("took too long"), null);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.Timeout, result.FailureKind);
Assert.Null(result.Cert);
}
[Fact]
public void OperationCanceledException_MapsToTimeout()
{
var result = RealOpcUaClient.MapVerifyOutcome(new OperationCanceledException(), null);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.Timeout, result.FailureKind);
Assert.Null(result.Cert);
}
[Fact]
public void ServiceResultException_BadRequestTimeout_MapsToTimeout()
{
var ex = new ServiceResultException(StatusCodes.BadRequestTimeout, "request timed out");
var result = RealOpcUaClient.MapVerifyOutcome(ex, null);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.Timeout, result.FailureKind);
}
[Theory]
[InlineData(StatusCodes.BadUserAccessDenied)]
[InlineData(StatusCodes.BadIdentityTokenRejected)]
[InlineData(StatusCodes.BadIdentityTokenInvalid)]
public void ServiceResultException_AuthCodes_MapToAuthFailed(uint statusCode)
{
var ex = new ServiceResultException(statusCode, "access denied");
var result = RealOpcUaClient.MapVerifyOutcome(ex, null);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.AuthFailed, result.FailureKind);
Assert.Null(result.Cert);
}
[Theory]
[InlineData(StatusCodes.BadConnectionRejected)]
[InlineData(StatusCodes.BadNotConnected)]
public void ServiceResultException_ConnectionCodes_MapToUnreachable(uint statusCode)
{
var ex = new ServiceResultException(statusCode, "no route");
var result = RealOpcUaClient.MapVerifyOutcome(ex, null);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.Unreachable, result.FailureKind);
}
[Fact]
public void SocketException_MapsToUnreachable()
{
var ex = new System.Net.Sockets.SocketException(
(int)System.Net.Sockets.SocketError.ConnectionRefused);
var result = RealOpcUaClient.MapVerifyOutcome(ex, null);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.Unreachable, result.FailureKind);
}
[Fact]
public void SocketExceptionWrappedInServiceResult_MapsToUnreachable()
{
// The SDK surfaces a refused/DNS connect as a generic ServiceResultException whose
// inner exception is the socket failure; the inner socket cause must dominate.
var inner = new System.Net.Sockets.SocketException(
(int)System.Net.Sockets.SocketError.HostNotFound);
var ex = new ServiceResultException(StatusCodes.BadUnexpectedError, "connect failed", inner);
var result = RealOpcUaClient.MapVerifyOutcome(ex, null);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.Unreachable, result.FailureKind);
}
[Fact]
public void GenericException_MapsToServerError()
{
var result = RealOpcUaClient.MapVerifyOutcome(new InvalidOperationException("boom"), null);
Assert.False(result.Success);
Assert.Equal(VerifyFailureKind.ServerError, result.FailureKind);
Assert.Null(result.Cert);
Assert.NotNull(result.Error);
}
[Fact]
public void NullException_NullCert_MapsToSuccess()
{
var result = RealOpcUaClient.MapVerifyOutcome(null, null);
Assert.True(result.Success);
Assert.Null(result.FailureKind);
Assert.Null(result.Error);
Assert.Null(result.Cert);
}
}