Guard HTTP fault handling when workflow reload returns null (#7714)
* Guard HTTP fault handling when workflow reload returns null * Add HTTP fault handler reload guard tests * Address Greptile review feedback
This commit is contained in:
parent
48a087e71e
commit
8f721e1ea2
|
|
@ -361,8 +361,8 @@ public class HttpWorkflowsMiddleware(RequestDelegate next)
|
|||
|
||||
var httpEndpointFaultHandler = serviceProvider.GetRequiredService<IHttpEndpointFaultHandler>();
|
||||
var workflowInstanceManager = serviceProvider.GetRequiredService<IWorkflowInstanceManager>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IWorkflowInstanceManager>();
|
||||
var httpEndpointFaultHandler = Substitute.For<IHttpEndpointFaultHandler>();
|
||||
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<CancellationToken>()).Returns(Task.FromResult<WorkflowInstance?>(workflowInstance));
|
||||
|
||||
var handled = await HandleWorkflowFaultAsync(serviceProvider, httpContext, CreateRunWorkflowResult(workflowState), CancellationToken.None);
|
||||
|
||||
Assert.True(handled);
|
||||
await httpEndpointFaultHandler.Received(1).HandleAsync(Arg.Is<HttpEndpointFaultContext>(context => ReferenceEquals(context.WorkflowState, reloadedWorkflowState)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleWorkflowFaultAsync_FallsBackToExecutionResultState_WhenReloadReturnsNull()
|
||||
{
|
||||
var workflowState = CreateFaultedWorkflowState("workflow-2");
|
||||
var workflowInstanceManager = Substitute.For<IWorkflowInstanceManager>();
|
||||
var httpEndpointFaultHandler = Substitute.For<IHttpEndpointFaultHandler>();
|
||||
var serviceProvider = new ServiceCollection()
|
||||
.AddSingleton(workflowInstanceManager)
|
||||
.AddSingleton(httpEndpointFaultHandler)
|
||||
.BuildServiceProvider();
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
||||
workflowInstanceManager.FindByIdAsync(workflowState.Id, Arg.Any<CancellationToken>()).Returns(Task.FromResult<WorkflowInstance?>(null));
|
||||
|
||||
var handled = await HandleWorkflowFaultAsync(serviceProvider, httpContext, CreateRunWorkflowResult(workflowState), CancellationToken.None);
|
||||
|
||||
Assert.True(handled);
|
||||
await httpEndpointFaultHandler.Received(1).HandleAsync(Arg.Is<HttpEndpointFaultContext>(context => ReferenceEquals(context.WorkflowState, workflowState)));
|
||||
}
|
||||
|
||||
private async Task<bool> HandleWorkflowFaultAsync(IServiceProvider serviceProvider, HttpContext httpContext, RunWorkflowResult workflowExecutionResult, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = (Task<bool>)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<ActivityIncident>
|
||||
{
|
||||
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<StoredBookmark> CreateCollidingHttpEndpointBookmarks()
|
||||
{
|
||||
yield return CreateBookmark("current-tenant-bookmark", CurrentTenantId);
|
||||
|
|
|
|||
Loading…
Reference in a new issue