using Elsa.Mediator.Contracts; using Elsa.Testing.Shared; using Elsa.Workflows.Activities; using Elsa.Workflows.Management; using Elsa.Workflows.Management.Entities; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Notifications; using Elsa.Workflows.Runtime.Requests; using Elsa.Workflows.State; using Microsoft.Extensions.Logging; using NSubstitute; namespace Elsa.Workflows.Runtime.UnitTests.Services; public class DefaultCommitStateHandlerTests { [Fact] public async Task CommitAsync_ExecutesPersistenceInsideCommitTransaction() { var fixture = await CommitTestFixture.CreateAsync(); fixture.WorkflowInstanceManager.SaveAsync(fixture.WorkflowState, Arg.Any()).Returns(fixture.WorkflowInstance); fixture.BookmarkPersister.When(x => x.PersistBookmarksAsync(Arg.Any())).Do(_ => Assert.True(fixture.Transaction.IsExecuting)); fixture.ActivityExecutionLogSink.When(x => x.PersistExecutionLogsAsync(fixture.WorkflowExecutionContext, Arg.Any())).Do(_ => Assert.True(fixture.Transaction.IsExecuting)); fixture.WorkflowExecutionLogSink.When(x => x.PersistExecutionLogsAsync(fixture.WorkflowExecutionContext, Arg.Any())).Do(_ => Assert.True(fixture.Transaction.IsExecuting)); fixture.VariablePersistenceManager.When(x => x.SaveVariablesAsync(fixture.WorkflowExecutionContext)).Do(_ => Assert.True(fixture.Transaction.IsExecuting)); fixture.WorkflowInstanceManager.When(x => x.SaveAsync(fixture.WorkflowState, Arg.Any())).Do(_ => Assert.True(fixture.Transaction.IsExecuting)); await fixture.Handler.CommitAsync(fixture.WorkflowExecutionContext, fixture.WorkflowState); Assert.True(fixture.Transaction.Completed); Assert.False(fixture.ActivityExecutionContext.IsDirty); await fixture.NotificationSender.Received(1).SendAsync( Arg.Is(x => x.WorkflowInstance == fixture.WorkflowInstance && x.WorkflowState == fixture.WorkflowState), Arg.Any()); } [Fact] public async Task CommitAsync_WhenPersistenceFails_DoesNotClearExecutionLogOrPublishCommittedNotification() { var fixture = await CommitTestFixture.CreateAsync(); fixture.WorkflowExecutionContext.AddExecutionLogEntry("Started"); fixture.WorkflowInstanceManager.SaveAsync(fixture.WorkflowState, Arg.Any()).Returns>(_ => throw new InvalidOperationException("state save failed")); await Assert.ThrowsAsync(() => fixture.Handler.CommitAsync(fixture.WorkflowExecutionContext, fixture.WorkflowState)); Assert.True(fixture.Transaction.Executed); Assert.False(fixture.Transaction.Completed); Assert.True(fixture.ActivityExecutionContext.IsDirty); Assert.NotEmpty(fixture.WorkflowExecutionContext.ExecutionLog); await fixture.NotificationSender.DidNotReceive().SendAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task CommitAsync_WhenBufferedNotificationFlushFails_PublishesCommittedNotification() { var fixture = await CommitTestFixture.CreateAsync(); var bufferedNotificationSender = new WorkflowCommitNotificationSender(fixture.Mediator, fixture.NotificationBuffer); var bufferedNotification = new TestNotification(); fixture.WorkflowInstanceManager.SaveAsync(fixture.WorkflowState, Arg.Any()).Returns(fixture.WorkflowInstance); fixture.ActivityExecutionLogSink .PersistExecutionLogsAsync(fixture.WorkflowExecutionContext, Arg.Any()) .Returns(_ => bufferedNotificationSender.SendAsync(bufferedNotification)); fixture.Mediator .SendAsync(bufferedNotification, Arg.Any(), Arg.Any()) .Returns(_ => throw new InvalidOperationException("Buffered handler failed")); await Assert.ThrowsAsync(() => fixture.Handler.CommitAsync(fixture.WorkflowExecutionContext, fixture.WorkflowState)); await fixture.NotificationSender.Received(1).SendAsync( Arg.Is(x => x.WorkflowInstance == fixture.WorkflowInstance && x.WorkflowState == fixture.WorkflowState), Arg.Any()); } private class CommitTestFixture { private CommitTestFixture(ActivityExecutionContext activityExecutionContext) { ActivityExecutionContext = activityExecutionContext; ActivityExecutionContext.Taint(); WorkflowExecutionContext = activityExecutionContext.WorkflowExecutionContext; WorkflowExecutionContext.AddActivityExecutionContext(activityExecutionContext); WorkflowState = new() { Id = WorkflowExecutionContext.Id }; WorkflowInstance = new() { Id = WorkflowExecutionContext.Id }; NotificationBuffer = CreateBuffer(Mediator); Handler = new( WorkflowInstanceManager, BookmarkPersister, VariablePersistenceManager, Transaction, NotificationBuffer, NotificationSender, ActivityExecutionLogSink, WorkflowExecutionLogSink); } public WorkflowExecutionContext WorkflowExecutionContext { get; } public ActivityExecutionContext ActivityExecutionContext { get; } public WorkflowState WorkflowState { get; } public WorkflowInstance WorkflowInstance { get; } public IWorkflowInstanceManager WorkflowInstanceManager { get; } = Substitute.For(); public IBookmarksPersister BookmarkPersister { get; } = Substitute.For(); public IVariablePersistenceManager VariablePersistenceManager { get; } = Substitute.For(); public IMediator Mediator { get; } = Substitute.For(); public WorkflowCommitNotificationBuffer NotificationBuffer { get; } public INotificationSender NotificationSender { get; } = Substitute.For(); public ILogRecordSink ActivityExecutionLogSink { get; } = Substitute.For>(); public ILogRecordSink WorkflowExecutionLogSink { get; } = Substitute.For>(); public RecordingWorkflowCommitTransaction Transaction { get; } = new(); public DefaultCommitStateHandler Handler { get; } public static async Task CreateAsync() { var fixture = new ActivityTestFixture(new WriteLine("Test")); var activityExecutionContext = await fixture.BuildAsync(); return new(activityExecutionContext); } } private static WorkflowCommitNotificationBuffer CreateBuffer(IMediator mediator) => new(mediator, Substitute.For>()); private class TestNotification : INotification; private class RecordingWorkflowCommitTransaction : IWorkflowCommitTransaction { public bool IsExecuting { get; private set; } public bool Executed { get; private set; } public bool Completed { get; private set; } public async Task ExecuteAsync(Func operation, CancellationToken cancellationToken = default) { Executed = true; IsExecuting = true; try { await operation(cancellationToken); Completed = true; } finally { IsExecuting = false; } } } }