diff --git a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs index 577a062ff..019a169c9 100644 --- a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs +++ b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs @@ -361,8 +361,8 @@ public class HttpWorkflowsMiddleware(RequestDelegate next) var httpEndpointFaultHandler = serviceProvider.GetRequiredService(); var workflowInstanceManager = serviceProvider.GetRequiredService(); - var workflowState = (await workflowInstanceManager.FindByIdAsync(workflowExecutionResult.WorkflowState.Id, cancellationToken))!; - await httpEndpointFaultHandler.HandleAsync(new(httpContext, workflowState.WorkflowState, cancellationToken)); + var workflowState = (await workflowInstanceManager.FindByIdAsync(workflowExecutionResult.WorkflowState.Id, cancellationToken))?.WorkflowState ?? workflowExecutionResult.WorkflowState; + await httpEndpointFaultHandler.HandleAsync(new(httpContext, workflowState, cancellationToken)); return true; } diff --git a/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs b/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs index 90eac8bed..69ec35e6d 100644 --- a/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs +++ b/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs @@ -4,18 +4,24 @@ using Elsa.Http.Bookmarks; using Elsa.Http.Middleware; using Elsa.Http.Options; using Elsa.Workflows; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Models; using Elsa.Workflows.Runtime; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; +using Elsa.Workflows.State; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using NSubstitute; namespace Elsa.Http.UnitTests.Middleware; public class HttpWorkflowsMiddlewareTests { - private static readonly MethodInfo ExecuteWithinTimeoutAsyncMethod = typeof(HttpWorkflowsMiddleware).GetMethod("ExecuteWithinTimeoutAsync", BindingFlags.Instance | BindingFlags.NonPublic)!; + private static readonly MethodInfo HandleWorkflowFaultAsyncMethod = GetRequiredPrivateMethod("HandleWorkflowFaultAsync"); + private static readonly MethodInfo ExecuteWithinTimeoutAsyncMethod = GetRequiredPrivateMethod("ExecuteWithinTimeoutAsync"); private const string CurrentTenantId = "tenant-a"; private const string OtherTenantId = "tenant-b"; private const string BookmarkHash = "http-endpoint:/colliding:get"; @@ -56,6 +62,73 @@ public class HttpWorkflowsMiddlewareTests Assert.False(filter.TenantAgnostic); } + [Fact] + public async Task HandleWorkflowFaultAsync_UsesReloadedWorkflowState_WhenAvailable() + { + var workflowState = CreateFaultedWorkflowState("workflow-1"); + var reloadedWorkflowState = CreateFaultedWorkflowState(workflowState.Id); + var workflowInstanceManager = Substitute.For(); + var httpEndpointFaultHandler = Substitute.For(); + var serviceProvider = new ServiceCollection() + .AddSingleton(workflowInstanceManager) + .AddSingleton(httpEndpointFaultHandler) + .BuildServiceProvider(); + var httpContext = new DefaultHttpContext(); + var workflowInstance = new WorkflowInstance + { + Id = workflowState.Id, + DefinitionId = workflowState.DefinitionId, + DefinitionVersionId = workflowState.DefinitionVersionId, + WorkflowState = reloadedWorkflowState + }; + + workflowInstanceManager.FindByIdAsync(workflowState.Id, Arg.Any()).Returns(Task.FromResult(workflowInstance)); + + var handled = await HandleWorkflowFaultAsync(serviceProvider, httpContext, CreateRunWorkflowResult(workflowState), CancellationToken.None); + + Assert.True(handled); + await httpEndpointFaultHandler.Received(1).HandleAsync(Arg.Is(context => ReferenceEquals(context.WorkflowState, reloadedWorkflowState))); + } + + [Fact] + public async Task HandleWorkflowFaultAsync_FallsBackToExecutionResultState_WhenReloadReturnsNull() + { + var workflowState = CreateFaultedWorkflowState("workflow-2"); + var workflowInstanceManager = Substitute.For(); + var httpEndpointFaultHandler = Substitute.For(); + var serviceProvider = new ServiceCollection() + .AddSingleton(workflowInstanceManager) + .AddSingleton(httpEndpointFaultHandler) + .BuildServiceProvider(); + var httpContext = new DefaultHttpContext(); + + workflowInstanceManager.FindByIdAsync(workflowState.Id, Arg.Any()).Returns(Task.FromResult(null)); + + var handled = await HandleWorkflowFaultAsync(serviceProvider, httpContext, CreateRunWorkflowResult(workflowState), CancellationToken.None); + + Assert.True(handled); + await httpEndpointFaultHandler.Received(1).HandleAsync(Arg.Is(context => ReferenceEquals(context.WorkflowState, workflowState))); + } + + private async Task HandleWorkflowFaultAsync(IServiceProvider serviceProvider, HttpContext httpContext, RunWorkflowResult workflowExecutionResult, CancellationToken cancellationToken) + { + var task = (Task)HandleWorkflowFaultAsyncMethod.Invoke(_middleware, [serviceProvider, httpContext, workflowExecutionResult, cancellationToken])!; + return await task; + } + + private static RunWorkflowResult CreateRunWorkflowResult(WorkflowState workflowState) => new(default!, workflowState, default!, null, Journal.Empty); + + private static WorkflowState CreateFaultedWorkflowState(string id) => new() + { + Id = id, + DefinitionId = "definition", + DefinitionVersionId = "definition-version", + Incidents = new List + { + new("activity", "activity-node", "TestActivity", "Boom", null, DateTimeOffset.UtcNow) + } + }; + [Fact] public async Task ExecuteWithinTimeoutAsync_RestoresRequestAbortedAfterSuccess() { @@ -113,6 +186,9 @@ public class HttpWorkflowsMiddlewareTests return await task; } + private static MethodInfo GetRequiredPrivateMethod(string name) => + typeof(HttpWorkflowsMiddleware).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new MissingMethodException(typeof(HttpWorkflowsMiddleware).FullName, name); + private static IEnumerable CreateCollidingHttpEndpointBookmarks() { yield return CreateBookmark("current-tenant-bookmark", CurrentTenantId);