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