using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NSubstitute; using ScadaLink.Commons.Entities.Deployment; using ScadaLink.Commons.Entities.Instances; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Commons.Types; using ScadaLink.Commons.Types.Enums; using ScadaLink.Commons.Types.Flattening; using ScadaLink.Communication; using ScadaLink.TemplateEngine.Flattening; namespace ScadaLink.DeploymentManager.Tests; /// /// CentralUI-006: regression tests proving /// raises whenever it /// writes a deployment record's status. This is the event source the Central /// UI deployment-status page subscribes to instead of polling. /// public class DeploymentStatusNotifierTests : TestKit { private readonly IDeploymentManagerRepository _repo; private readonly IFlatteningPipeline _pipeline; private readonly CommunicationService _comms; private readonly OperationLockManager _lockManager; private readonly IAuditService _audit; private readonly DeploymentStatusNotifier _notifier; private readonly DeploymentService _service; public DeploymentStatusNotifierTests() { _repo = Substitute.For(); _pipeline = Substitute.For(); _comms = new CommunicationService( Options.Create(new CommunicationOptions()), NullLogger.Instance); _lockManager = new OperationLockManager(); _audit = Substitute.For(); _notifier = new DeploymentStatusNotifier(NullLogger.Instance); var options = Options.Create(new DeploymentManagerOptions { OperationLockTimeout = TimeSpan.FromSeconds(5) }); var siteRepo = Substitute.For(); _service = new DeploymentService( _repo, siteRepo, _pipeline, _comms, _lockManager, _audit, new DiffService(), _notifier, options, NullLogger.Instance); } [Fact] public async Task DeployInstanceAsync_RaisesStatusChange_ForEveryRecordStatusWrite() { var instance = new Instance("TestInst") { Id = 7, SiteId = 1, State = InstanceState.NotDeployed }; _repo.GetInstanceByIdAsync(7).Returns(instance); var config = new FlattenedConfiguration { InstanceUniqueName = "TestInst" }; _pipeline.FlattenAndValidateAsync(7, Arg.Any()) .Returns(Result.Success( new FlatteningPipelineResult(config, "sha256:abc", ValidationResult.Success()))); var changes = new List(); _notifier.StatusChanged += c => changes.Add(c); // _comms has no actor set, so the deploy reaches the catch block and // the record ends Failed. The notifier must fire for the Pending, // InProgress and Failed writes — not be silent (the pre-fix behaviour). var result = await _service.DeployInstanceAsync(7, "admin"); Assert.True(result.IsFailure); Assert.NotEmpty(changes); Assert.All(changes, c => Assert.Equal(7, c.InstanceId)); Assert.Contains(changes, c => c.Status == DeploymentStatus.Pending); Assert.Contains(changes, c => c.Status == DeploymentStatus.InProgress); Assert.Contains(changes, c => c.Status == DeploymentStatus.Failed); // All notifications carry the same deployment id (the one created here). var deploymentId = changes[0].DeploymentId; Assert.False(string.IsNullOrEmpty(deploymentId)); Assert.All(changes, c => Assert.Equal(deploymentId, c.DeploymentId)); } [Fact] public void NotifyStatusChanged_WithNoSubscribers_DoesNotThrow() { var notifier = new DeploymentStatusNotifier(NullLogger.Instance); var ex = Record.Exception(() => notifier.NotifyStatusChanged(new DeploymentStatusChange("dep-1", 1, DeploymentStatus.Success))); Assert.Null(ex); } [Fact] public void NotifyStatusChanged_ThrowingSubscriber_DoesNotBreakOtherSubscribers() { var notifier = new DeploymentStatusNotifier(NullLogger.Instance); var reached = false; notifier.StatusChanged += _ => throw new InvalidOperationException("boom"); notifier.StatusChanged += _ => reached = true; var ex = Record.Exception(() => notifier.NotifyStatusChanged(new DeploymentStatusChange("dep-2", 2, DeploymentStatus.InProgress))); Assert.Null(ex); Assert.True(reached, "a faulting subscriber must not stop later subscribers from being notified"); } }