181 lines
6.9 KiB
C#
181 lines
6.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// T17: Unit tests for <see cref="RealOpcUaClient.MapVerifyOutcome"/> — the pure
|
|
/// (exception + captured cert) → <see cref="VerifyEndpointResult"/> mapping that backs the
|
|
/// OPC UA verify-endpoint probe. These exercise every <see cref="VerifyFailureKind"/>
|
|
/// branch WITHOUT a live OPC UA server, so the classification logic is covered
|
|
/// deterministically.
|
|
/// </summary>
|
|
public class VerifyEndpointResultMappingTests
|
|
{
|
|
/// <summary>
|
|
/// Builds a throwaway self-signed certificate so the untrusted-cert branch can be
|
|
/// exercised with a real <see cref="X509Certificate2"/> (and real DER bytes) without a
|
|
/// live server. Not persisted; only its public fields are asserted.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|