using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Akka.Actor; using Akka.TestKit.Xunit2; using FluentAssertions; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.DataConnectionLayer; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors; /// /// T17 / D6: per-node CertStoreActor write/list/remove round-trip against a /// temp trusted-peer PKI store. The cluster broadcast (singleton → every node's /// CertStoreActor) is validated live at integration E1; here we cover the /// testable per-node core. /// public class CertStoreActorTests : TestKit, IDisposable { private readonly string _trustedDir; private readonly string _rejectedDir; private readonly OpcUaGlobalOptions _options; public CertStoreActorTests() { var root = Path.Combine(Path.GetTempPath(), "ScadaBridge-CertStoreTests", Guid.NewGuid().ToString("N")); _trustedDir = Path.Combine(root, "trusted"); _rejectedDir = Path.Combine(root, "rejected"); _options = new OpcUaGlobalOptions { TrustedPeerStorePath = _trustedDir, RejectedCertificateStorePath = _rejectedDir, }; } void IDisposable.Dispose() { Shutdown(); TryDeleteDir(Path.GetDirectoryName(_trustedDir)!); } private static void TryDeleteDir(string dir) { try { if (Directory.Exists(dir)) { Directory.Delete(dir, recursive: true); } } catch { // Best-effort temp cleanup. } } private static (string DerBase64, string Thumbprint) BuildSelfSignedCert() { using var rsa = RSA.Create(2048); var req = new CertificateRequest( "CN=ScadaBridge-CertStoreTest", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); using var cert = req.CreateSelfSigned( DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(365)); var der = cert.Export(X509ContentType.Cert); return (Convert.ToBase64String(der), cert.Thumbprint); } [Fact] public void Write_ThenList_ThenRemove_RoundTrips() { var (derBase64, thumbprint) = BuildSelfSignedCert(); var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options))); // Write actor.Tell(new WriteCertToLocalStore(derBase64, thumbprint)); var writeAck = ExpectMsg(); writeAck.Success.Should().BeTrue(); writeAck.Error.Should().BeNull(); var expectedFile = Path.Combine(_trustedDir, $"{thumbprint}.der"); File.Exists(expectedFile).Should().BeTrue("the cert is written into the trusted-peer store"); // List — the cert appears, not rejected actor.Tell(new ListLocalCerts()); var listAck = ExpectMsg(); listAck.Success.Should().BeTrue(); listAck.Certs.Should().NotBeNull(); listAck.Certs!.Should().ContainSingle(c => c.Thumbprint == thumbprint) .Which.Rejected.Should().BeFalse(); // Remove — ack + file gone actor.Tell(new RemoveCertFromLocalStore(thumbprint)); var removeAck = ExpectMsg(); removeAck.Success.Should().BeTrue(); File.Exists(expectedFile).Should().BeFalse("the cert file is deleted from the trusted-peer store"); // List again — empty actor.Tell(new ListLocalCerts()); var emptyAck = ExpectMsg(); emptyAck.Success.Should().BeTrue(); emptyAck.Certs.Should().NotBeNull(); emptyAck.Certs!.Should().NotContain(c => c.Thumbprint == thumbprint); } [Fact] public void Write_InvalidBase64_ReturnsFailureAck() { var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options))); actor.Tell(new WriteCertToLocalStore("not-valid-base64!!", "ABC123")); var ack = ExpectMsg(); ack.Success.Should().BeFalse(); ack.Error.Should().NotBeNullOrEmpty(); } [Fact] public void Remove_MissingCert_IsNoOpSuccess() { var actor = Sys.ActorOf(Props.Create(() => new CertStoreActor(_options))); actor.Tell(new RemoveCertFromLocalStore("NONEXISTENTTHUMBPRINT")); var ack = ExpectMsg(); ack.Success.Should().BeTrue("removing an absent cert is an idempotent no-op"); } }