Skip to content

Commit

Permalink
Add new HTTP workflow tests and enhance middleware (#5908)
Browse files Browse the repository at this point in the history
Introduce tests for resuming specific HTTP workflow instances. Enhanced `HttpWorkflowsMiddleware` to handle workflow instance IDs and correlation IDs for more precise control over workflow activation and resumption.
  • Loading branch information
sfmskywalker committed Aug 16, 2024
1 parent 0a76741 commit edf365b
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 15 deletions.
25 changes: 13 additions & 12 deletions src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,16 @@ public async Task InvokeAsync(HttpContext httpContext, IServiceProvider serviceP
var request = httpContext.Request;
var method = request.Method.ToLowerInvariant();
var httpWorkflowLookupService = serviceProvider.GetRequiredService<IHttpWorkflowLookupService>();
var workflowInstanceId = await GetWorkflowInstanceIdAsync(serviceProvider, httpContext, cancellationToken);
var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, cancellationToken);
var bookmarkHash = ComputeBookmarkHash(serviceProvider, matchingPath, method);
var lookupResult = await httpWorkflowLookupService.FindWorkflowAsync(bookmarkHash, cancellationToken);

if (lookupResult != null)
{
var triggers = lookupResult.Triggers;

if (triggers?.Count > 1)
if (triggers.Count > 1)
{
await HandleMultipleWorkflowsFoundAsync(httpContext, () => triggers.Select(x => new
{
Expand All @@ -85,16 +87,16 @@ public async Task InvokeAsync(HttpContext httpContext, IServiceProvider serviceP
return;
}

var trigger = triggers?.FirstOrDefault();
var trigger = triggers.FirstOrDefault();
if (trigger != null)
{
var workflowGraph = lookupResult.WorkflowGraph!;
await StartWorkflowAsync(httpContext, trigger, workflowGraph, input);
await StartWorkflowAsync(httpContext, trigger, workflowGraph, workflowInstanceId, correlationId, input);
return;
}
}

var bookmarks = await FindBookmarksAsync(serviceProvider, bookmarkHash, cancellationToken).ToList();
var bookmarks = await FindBookmarksAsync(serviceProvider, bookmarkHash, workflowInstanceId, correlationId, cancellationToken).ToList();

if (bookmarks.Count > 1)
{
Expand All @@ -109,7 +111,7 @@ public async Task InvokeAsync(HttpContext httpContext, IServiceProvider serviceP

if (bookmark != null)
{
await ResumeWorkflowAsync(httpContext, bookmark, input);
await ResumeWorkflowAsync(httpContext, bookmark, correlationId, input);
return;
}

Expand All @@ -131,7 +133,7 @@ public async Task InvokeAsync(HttpContext httpContext, IServiceProvider serviceP
return await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, cancellationToken);
}

private async Task StartWorkflowAsync(HttpContext httpContext, StoredTrigger trigger, WorkflowGraph workflowGraph, IDictionary<string, object> input)
private async Task StartWorkflowAsync(HttpContext httpContext, StoredTrigger trigger, WorkflowGraph workflowGraph, string? workflowInstanceId, string? correlationId, Dictionary<string, object>? input)
{
var serviceProvider = httpContext.RequestServices;
var cancellationToken = httpContext.RequestAborted;
Expand All @@ -144,8 +146,6 @@ private async Task StartWorkflowAsync(HttpContext httpContext, StoredTrigger tri
await ExecuteWithinTimeoutAsync(async ct =>
{
var cancellationTokens = new CancellationTokens(ct, ct);
var workflowInstanceId = await GetWorkflowInstanceIdAsync(serviceProvider, httpContext, httpContext.RequestAborted);
var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, httpContext.RequestAborted);
var startParams = new StartWorkflowHostParams
{
Input = input,
Expand All @@ -160,7 +160,7 @@ await ExecuteWithinTimeoutAsync(async ct =>
await HandleWorkflowFaultAsync(serviceProvider, httpContext, workflowHost.WorkflowState, cancellationToken);
}

private async Task ResumeWorkflowAsync(HttpContext httpContext, StoredBookmark bookmark, IDictionary<string, object> input)
private async Task ResumeWorkflowAsync(HttpContext httpContext, StoredBookmark bookmark, string? correlationId, IDictionary<string, object> input)
{
var serviceProvider = httpContext.RequestServices;
var cancellationToken = httpContext.RequestAborted;
Expand Down Expand Up @@ -193,7 +193,6 @@ private async Task ResumeWorkflowAsync(HttpContext httpContext, StoredBookmark b

await ExecuteWithinTimeoutAsync(async ct =>
{
var correlationId = await GetCorrelationIdAsync(serviceProvider, httpContext, ct);
var cancellationTokens = new CancellationTokens(ct, ct);
var resumeParams = new ResumeWorkflowHostParams
{
Expand All @@ -219,12 +218,14 @@ private async Task<IEnumerable<StoredTrigger>> FindTriggersAsync(IServiceProvide
return await triggerStore.FindManyAsync(triggerFilter, cancellationToken);
}

private async Task<IEnumerable<StoredBookmark>> FindBookmarksAsync(IServiceProvider serviceProvider, string bookmarkHash, CancellationToken cancellationToken)
private async Task<IEnumerable<StoredBookmark>> FindBookmarksAsync(IServiceProvider serviceProvider, string bookmarkHash, string? workflowInstanceId, string? correlationId, CancellationToken cancellationToken)
{
var bookmarkStore = serviceProvider.GetRequiredService<IBookmarkStore>();
var bookmarkFilter = new BookmarkFilter
{
Hash = bookmarkHash
Hash = bookmarkHash,
WorkflowInstanceId = workflowInstanceId,
CorrelationId = correlationId
};
return await bookmarkStore.FindManyAsync(bookmarkFilter, cancellationToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@
<None Update="Scenarios\WorkflowCompletion\fork.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\HttpWorkflows\http-hello-world.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\LogPersistenceModes\input-output-logging-1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down Expand Up @@ -81,6 +78,9 @@
<None Update="Scenarios\WorkflowDefinitionReload\Workflows\http-workflow.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\HttpWorkflows\Workflows\http-hello-world.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Net;
using Elsa.Workflows.Contracts;
using Microsoft.Extensions.DependencyInjection;

namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows;

public class ResumeSpecificHttpWorkflowInstanceTests(App app) : AppComponentTest(app)
{
[Theory]
[InlineData("workflowInstanceId")]
[InlineData("correlationId")]
public async Task ResumingSpecificWorkflow_ShouldResumeSpecifiedWorkflow(string identifierKey)
{
var client = WorkflowServer.CreateHttpWorkflowClient();

// Start 3 instances.
var workflowInstanceId1 = await StartWorkflowAsync(client, identifierKey);
var workflowInstanceId2 = await StartWorkflowAsync(client, identifierKey);
var workflowInstanceId3 = await StartWorkflowAsync(client, identifierKey);

// Resume the 2nd instance.
var response = await ResumeWorkflowAsync(client, identifierKey, workflowInstanceId2);

// Response should be OK.
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

private async Task<string> StartWorkflowAsync(HttpClient client, string identifierKey)
{
var identityGenerator = Scope.ServiceProvider.GetRequiredService<IIdentityGenerator>();
var identifierValue = identityGenerator.GenerateId();
await client.GetStringAsync($"simple-http-api/start?{identifierKey}={identifierValue}");
return identifierValue;
}

private async Task<HttpResponseMessage> ResumeWorkflowAsync(HttpClient client, string identifierKey, string identifierValue)
{
return await client.GetAsync($"simple-http-api/resume?{identifierKey}={identifierValue}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Elsa.Http;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Microsoft.AspNetCore.Http;

namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows.Workflows;

public class SimpleHttpApiWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new Sequence
{
Activities =
[
new HttpEndpoint
{
Path = new("simple-http-api/start"),
SupportedMethods = new([HttpMethods.Get]),
CanStartWorkflow = true
},
new HttpEndpoint
{
Path = new("simple-http-api/resume"),
SupportedMethods = new([HttpMethods.Get])
}
]
};
}
}

0 comments on commit edf365b

Please sign in to comment.