Adds ExecuteWorkflow activity tests (#7003)
* Add unit tests for `ExecuteWorkflow` activity covering various scenarios and refactor `FlowJoin` activity to remove `[Obsolete]` attribute. * Add integration tests for `ExecuteWorkflow` activity - Introduce parent and child workflow definitions to test various scenarios, including input handling, output capture, correlation ID setting, and execution order. - Add corresponding JSON workflow files for integration tests. - Update project files to include new workflows. * Refactor `ExecuteWorkflowTests` to consolidate repetitive code by introducing utility methods for workflow execution and instance retrieval. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
1423728769
commit
a5b760ec09
|
|
@ -0,0 +1,112 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.State;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Activities.ExecuteWorkflow;
|
||||
|
||||
public class ExecuteWorkflowTests
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
|
||||
public ExecuteWorkflowTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_services = new TestApplicationBuilder(testOutputHelper)
|
||||
.WithCapturingTextWriter(_capturingTextWriter)
|
||||
.WithWorkflowsFromDirectory("Activities", "ExecuteWorkflow", "Workflows")
|
||||
.Build();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ExecuteWorkflow without WaitForCompletion completes immediately")]
|
||||
public async Task ExecuteWorkflowWithoutWaitForCompletion()
|
||||
{
|
||||
var workflowState = await RunWorkflowAsync("parent-no-wait");
|
||||
|
||||
Assert.Equal(WorkflowStatus.Finished, workflowState.Status);
|
||||
Assert.Equal(WorkflowSubStatus.Finished, workflowState.SubStatus);
|
||||
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Contains("Parent: Before child", lines);
|
||||
Assert.Contains("Child: Executing", lines);
|
||||
Assert.Contains("Parent: After child", lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ExecuteWorkflow with WaitForCompletion waits for child to finish")]
|
||||
public async Task ExecuteWorkflowWithWaitForCompletion()
|
||||
{
|
||||
var workflowState = await RunWorkflowAsync("parent-with-wait");
|
||||
|
||||
Assert.Equal(WorkflowStatus.Finished, workflowState.Status);
|
||||
Assert.Equal(WorkflowSubStatus.Finished, workflowState.SubStatus);
|
||||
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Equal(3, lines.Count);
|
||||
Assert.Equal("Parent: Before child", lines[0]);
|
||||
Assert.Equal("Child: Executing", lines[1]);
|
||||
Assert.Equal("Parent: After child", lines[2]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ExecuteWorkflow passes input to child workflow")]
|
||||
public async Task ExecuteWorkflowPassesInput()
|
||||
{
|
||||
var workflowState = await RunWorkflowAsync("parent-with-input");
|
||||
|
||||
Assert.Equal(WorkflowStatus.Finished, workflowState.Status);
|
||||
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Contains("Child received: Hello from parent", lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ExecuteWorkflow captures child workflow output")]
|
||||
public async Task ExecuteWorkflowCapturesOutput()
|
||||
{
|
||||
var workflowState = await RunWorkflowAsync("parent-capture-output");
|
||||
|
||||
Assert.Equal(WorkflowStatus.Finished, workflowState.Status);
|
||||
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Contains("Child output: 42", lines);
|
||||
Assert.Contains("Parent received: 42", lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ExecuteWorkflow sets correlation ID on child workflow")]
|
||||
public async Task ExecuteWorkflowSetsCorrelationId()
|
||||
{
|
||||
var workflowState = await RunWorkflowAsync("parent-with-correlation");
|
||||
|
||||
Assert.Equal(WorkflowStatus.Finished, workflowState.Status);
|
||||
|
||||
var childInstance = await GetWorkflowInstanceAsync("child-workflow");
|
||||
Assert.Equal("test-correlation-123", childInstance.CorrelationId);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ExecuteWorkflow includes ParentInstanceId in child properties")]
|
||||
public async Task ExecuteWorkflowSetsParentInstanceId()
|
||||
{
|
||||
await RunWorkflowAsync("parent-no-wait");
|
||||
|
||||
var parentInstance = await GetWorkflowInstanceAsync("parent-no-wait");
|
||||
var childInstance = await GetWorkflowInstanceAsync("child-workflow");
|
||||
|
||||
Assert.True(childInstance.WorkflowState.Properties.ContainsKey("ParentInstanceId"));
|
||||
Assert.Equal(parentInstance.Id, childInstance.WorkflowState.Properties["ParentInstanceId"]);
|
||||
}
|
||||
|
||||
private async Task<WorkflowState> RunWorkflowAsync(string workflowDefinitionId)
|
||||
{
|
||||
await _services.PopulateRegistriesAsync();
|
||||
return await _services.RunWorkflowUntilEndAsync(workflowDefinitionId);
|
||||
}
|
||||
|
||||
private async Task<WorkflowInstance> GetWorkflowInstanceAsync(string definitionId)
|
||||
{
|
||||
var workflowInstanceStore = _services.GetRequiredService<IWorkflowInstanceStore>();
|
||||
return (await workflowInstanceStore.FindAsync(new()
|
||||
{
|
||||
DefinitionId = definitionId
|
||||
}))!;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"id": "child-with-input-v1",
|
||||
"definitionId": "child-with-input",
|
||||
"name": "Child With Input",
|
||||
"createdAt": "2025-01-15T00:00:00.000000+00:00",
|
||||
"version": 1,
|
||||
"variables": [],
|
||||
"inputs": [
|
||||
{
|
||||
"uiHint": "singleline",
|
||||
"storageDriverType": "Elsa.Workflows.Services.WorkflowStorageDriver, Elsa.Workflows.Core",
|
||||
"type": "String",
|
||||
"name": "Message",
|
||||
"displayName": "Message",
|
||||
"description": "",
|
||||
"category": "",
|
||||
"isArray": false
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"id": "Flowchart1",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"source": "FlowchartJsonConverter.cs:45",
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"start": "WriteLine1",
|
||||
"activities": [
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "JavaScript",
|
||||
"value": "`Child received: ${getMessage()}`"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "WriteLine1:input-1"
|
||||
}
|
||||
},
|
||||
"id": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"id": "child-with-output-v1",
|
||||
"definitionId": "child-with-output",
|
||||
"name": "Child With Output",
|
||||
"createdAt": "2025-01-15T00:00:00.000000+00:00",
|
||||
"version": 1,
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"type": "Object",
|
||||
"name": "Result",
|
||||
"displayName": "Result",
|
||||
"description": "",
|
||||
"isArray": false
|
||||
}
|
||||
],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"id": "Flowchart1",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"source": "FlowchartJsonConverter.cs:45",
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"start": "WriteLine1",
|
||||
"activities": [
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Child output: 42"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "WriteLine1:input-1"
|
||||
}
|
||||
},
|
||||
"id": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"outputName": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Result"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "SetOutput1:outputName"
|
||||
}
|
||||
},
|
||||
"outputValue": {
|
||||
"typeName": "Object",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": 42
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "SetOutput1:outputValue"
|
||||
}
|
||||
},
|
||||
"id": "SetOutput1",
|
||||
"type": "Elsa.SetOutput",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"source": {
|
||||
"activity": "WriteLine1",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "SetOutput1",
|
||||
"port": "In"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "child-workflow-v1",
|
||||
"definitionId": "child-workflow",
|
||||
"name": "Child Workflow",
|
||||
"createdAt": "2025-01-15T00:00:00.000000+00:00",
|
||||
"version": 1,
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"id": "Flowchart1",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"source": "FlowchartJsonConverter.cs:45",
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"start": "WriteLine1",
|
||||
"activities": [
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Child: Executing"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "WriteLine1:input-1"
|
||||
}
|
||||
},
|
||||
"id": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"id": "parent-capture-output-v1",
|
||||
"definitionId": "parent-capture-output",
|
||||
"name": "Parent Capture Output",
|
||||
"createdAt": "2025-01-15T00:00:00.000000+00:00",
|
||||
"version": 1,
|
||||
"variables": [
|
||||
{
|
||||
"id": "ChildResult",
|
||||
"name": "ChildResult",
|
||||
"typeName": "Object",
|
||||
"isArray": false,
|
||||
"value": null,
|
||||
"storageDriverType": "Elsa.Workflows.Services.WorkflowStorageDriver, Elsa.Workflows.Core"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Sequence",
|
||||
"version": 1,
|
||||
"id": "Sequence1",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"activities": [
|
||||
{
|
||||
"workflowDefinitionId": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "child-with-output"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:workflowDefinitionId"
|
||||
}
|
||||
},
|
||||
"waitForCompletion": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": true
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:waitForCompletion"
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"typeName": "ExecuteWorkflowResult",
|
||||
"memoryReference": {
|
||||
"id": "ChildResult"
|
||||
}
|
||||
},
|
||||
"id": "ExecuteWorkflow1",
|
||||
"type": "Elsa.ExecuteWorkflow",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "JavaScript",
|
||||
"value": "`Parent received: ${getChildResult().output.Result}`"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "WriteLine1:input-1"
|
||||
}
|
||||
},
|
||||
"id": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"id": "parent-no-wait-v1",
|
||||
"definitionId": "parent-no-wait",
|
||||
"name": "Parent No Wait",
|
||||
"createdAt": "2025-01-15T00:00:00.000000+00:00",
|
||||
"version": 1,
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Sequence",
|
||||
"version": 1,
|
||||
"id": "Sequence1",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"activities": [
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Parent: Before child"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "WriteLine1:input-1"
|
||||
}
|
||||
},
|
||||
"id": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"workflowDefinitionId": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "child-workflow"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:workflowDefinitionId"
|
||||
}
|
||||
},
|
||||
"waitForCompletion": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": false
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:waitForCompletion"
|
||||
}
|
||||
},
|
||||
"id": "ExecuteWorkflow1",
|
||||
"type": "Elsa.ExecuteWorkflow",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Parent: After child"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "WriteLine2:input-1"
|
||||
}
|
||||
},
|
||||
"id": "WriteLine2",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"id": "parent-with-correlation-v1",
|
||||
"definitionId": "parent-with-correlation",
|
||||
"name": "Parent With Correlation",
|
||||
"createdAt": "2025-01-15T00:00:00.000000+00:00",
|
||||
"version": 1,
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"id": "Flowchart1",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"source": "FlowchartJsonConverter.cs:45",
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"start": "ExecuteWorkflow1",
|
||||
"activities": [
|
||||
{
|
||||
"workflowDefinitionId": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "child-workflow"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:workflowDefinitionId"
|
||||
}
|
||||
},
|
||||
"correlationId": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "test-correlation-123"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:correlationId"
|
||||
}
|
||||
},
|
||||
"waitForCompletion": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": true
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:waitForCompletion"
|
||||
}
|
||||
},
|
||||
"id": "ExecuteWorkflow1",
|
||||
"type": "Elsa.ExecuteWorkflow",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"id": "parent-with-input-v1",
|
||||
"definitionId": "parent-with-input",
|
||||
"name": "Parent With Input",
|
||||
"createdAt": "2025-01-15T00:00:00.000000+00:00",
|
||||
"version": 1,
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"id": "Flowchart1",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"source": "FlowchartJsonConverter.cs:45",
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"start": "ExecuteWorkflow1",
|
||||
"activities": [
|
||||
{
|
||||
"workflowDefinitionId": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "child-with-input"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:workflowDefinitionId"
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"typeName": "IDictionary<String, Object>",
|
||||
"expression": {
|
||||
"type": "JavaScript",
|
||||
"value": "({ Message: 'Hello from parent' })"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:input"
|
||||
}
|
||||
},
|
||||
"waitForCompletion": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": true
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:waitForCompletion"
|
||||
}
|
||||
},
|
||||
"id": "ExecuteWorkflow1",
|
||||
"type": "Elsa.ExecuteWorkflow",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"id": "parent-with-wait-v1",
|
||||
"definitionId": "parent-with-wait",
|
||||
"name": "Parent With Wait",
|
||||
"createdAt": "2025-01-15T00:00:00.000000+00:00",
|
||||
"version": 1,
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Sequence",
|
||||
"version": 1,
|
||||
"id": "Sequence1",
|
||||
"metadata": {},
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"activities": [
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Parent: Before child"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "WriteLine1:input-1"
|
||||
}
|
||||
},
|
||||
"id": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"workflowDefinitionId": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "child-workflow"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:workflowDefinitionId"
|
||||
}
|
||||
},
|
||||
"waitForCompletion": {
|
||||
"typeName": "Boolean",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": true
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ExecuteWorkflow1:waitForCompletion"
|
||||
}
|
||||
},
|
||||
"id": "ExecuteWorkflow1",
|
||||
"type": "Elsa.ExecuteWorkflow",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Parent: After child"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "WriteLine2:input-1"
|
||||
}
|
||||
},
|
||||
"id": "WriteLine2",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -62,5 +62,29 @@
|
|||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-published-version.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Activities\ExecuteWorkflow\Workflows\child-with-input.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Activities\ExecuteWorkflow\Workflows\child-with-output.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Activities\ExecuteWorkflow\Workflows\child-workflow.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Activities\ExecuteWorkflow\Workflows\parent-capture-output.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Activities\ExecuteWorkflow\Workflows\parent-no-wait.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Activities\ExecuteWorkflow\Workflows\parent-with-correlation.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Activities\ExecuteWorkflow\Workflows\parent-with-input.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Activities\ExecuteWorkflow\Workflows\parent-with-wait.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,268 @@
|
|||
using Elsa.Common.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Options;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Activities;
|
||||
using Elsa.Workflows.State;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Composition;
|
||||
|
||||
public class ExecuteWorkflowTests
|
||||
{
|
||||
private const string DefaultWorkflowDefinitionId = "test-workflow-def";
|
||||
private const string DefaultGeneratedId = "generated-child-id";
|
||||
private const string DefaultCorrelationId = "test-correlation";
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, WorkflowStatus.Finished)]
|
||||
[InlineData(true, WorkflowStatus.Running)]
|
||||
[InlineData(false, WorkflowStatus.Finished)]
|
||||
[InlineData(false, WorkflowStatus.Running)]
|
||||
public async Task Should_Invoke_Workflow_With_Correct_Options(bool waitForCompletion, WorkflowStatus childWorkflowStatus)
|
||||
{
|
||||
// Arrange
|
||||
var input = new Dictionary<string, object> { ["Key1"] = "Value1" };
|
||||
var childOutput = new Dictionary<string, object> { ["ChildKey"] = "ChildValue" };
|
||||
|
||||
var executeWorkflow = new ExecuteWorkflow
|
||||
{
|
||||
WorkflowDefinitionId = new(DefaultWorkflowDefinitionId),
|
||||
CorrelationId = new(DefaultCorrelationId),
|
||||
Input = new(input),
|
||||
WaitForCompletion = new(waitForCompletion)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (context, workflowInvoker) = await ExecuteAsync(executeWorkflow, childWorkflowStatus, childOutput);
|
||||
var parentInstanceId = context.WorkflowExecutionContext.Id;
|
||||
|
||||
// Assert
|
||||
await workflowInvoker.Received(1).InvokeAsync(
|
||||
Arg.Any<WorkflowGraph>(),
|
||||
Arg.Is<RunWorkflowOptions>(opts =>
|
||||
opts.ParentWorkflowInstanceId == parentInstanceId &&
|
||||
opts.WorkflowInstanceId == DefaultGeneratedId &&
|
||||
opts.CorrelationId == DefaultCorrelationId &&
|
||||
opts.Input != null && opts.Input.ContainsKey("Key1") && (string)opts.Input["Key1"] == "Value1" &&
|
||||
opts.Input.ContainsKey("ParentInstanceId") && (string)opts.Input["ParentInstanceId"] == parentInstanceId &&
|
||||
opts.Properties.ContainsKey("ParentInstanceId") && (string)opts.Properties["ParentInstanceId"] == parentInstanceId &&
|
||||
(waitForCompletion ? opts.Properties.ContainsKey("WaitForCompletion") && (bool)opts.Properties["WaitForCompletion"] : !opts.Properties.ContainsKey("WaitForCompletion"))
|
||||
),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
|
||||
// Result is only set when either not waiting or child workflow finishes
|
||||
if (!waitForCompletion || childWorkflowStatus == WorkflowStatus.Finished)
|
||||
{
|
||||
var result = (ExecuteWorkflowResult)context.GetActivityOutput(() => executeWorkflow.Result)!;
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(DefaultGeneratedId, result.WorkflowInstanceId);
|
||||
Assert.Equal(childWorkflowStatus, result.Status);
|
||||
Assert.Equal(childOutput, result.Output);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, WorkflowStatus.Running)]
|
||||
[InlineData(true, WorkflowStatus.Finished)]
|
||||
public async Task Should_Complete_Activity_When_Expected(bool waitForCompletion, WorkflowStatus childWorkflowStatus)
|
||||
{
|
||||
// Arrange
|
||||
var executeWorkflow = new ExecuteWorkflow
|
||||
{
|
||||
WorkflowDefinitionId = new(DefaultWorkflowDefinitionId),
|
||||
WaitForCompletion = new(waitForCompletion)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (context, _) = await ExecuteAsync(executeWorkflow, childWorkflowStatus);
|
||||
|
||||
// Assert - Activity should complete when not waiting or when child workflow finishes
|
||||
Assert.True(context.IsCompleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Create_Bookmark_When_WaitForCompletion_Is_True_And_Child_Is_Running()
|
||||
{
|
||||
// Arrange
|
||||
var executeWorkflow = new ExecuteWorkflow
|
||||
{
|
||||
WorkflowDefinitionId = new(DefaultWorkflowDefinitionId),
|
||||
WaitForCompletion = new(true)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (context, _) = await ExecuteAsync(executeWorkflow, WorkflowStatus.Running);
|
||||
|
||||
// Assert - Activity should not complete when child workflow is still running and waiting
|
||||
Assert.False(context.IsCompleted);
|
||||
Assert.NotEmpty(context.Bookmarks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Throw_When_Workflow_Definition_Not_Found()
|
||||
{
|
||||
// Arrange
|
||||
const string workflowDefinitionId = "non-existent-workflow";
|
||||
|
||||
var workflowDefinitionService = Substitute.For<IWorkflowDefinitionService>();
|
||||
workflowDefinitionService
|
||||
.FindWorkflowGraphAsync(workflowDefinitionId, Arg.Any<VersionOptions>(), Arg.Any<CancellationToken>())
|
||||
.Returns((WorkflowGraph?)null);
|
||||
|
||||
var executeWorkflow = new ExecuteWorkflow
|
||||
{
|
||||
WorkflowDefinitionId = new(workflowDefinitionId)
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<Exception>(() => ExecuteAsync(executeWorkflow, customWorkflowDefinitionService: workflowDefinitionService));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Use_Empty_Input_When_Input_Not_Provided()
|
||||
{
|
||||
// Arrange
|
||||
var executeWorkflow = new ExecuteWorkflow
|
||||
{
|
||||
WorkflowDefinitionId = new(DefaultWorkflowDefinitionId)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (_, workflowInvoker) = await ExecuteAsync(executeWorkflow);
|
||||
|
||||
// Assert - Should create input with ParentInstanceId even when no input provided
|
||||
await workflowInvoker.Received(1).InvokeAsync(
|
||||
Arg.Any<WorkflowGraph>(),
|
||||
Arg.Is<RunWorkflowOptions>(opts =>
|
||||
opts.Input != null &&
|
||||
opts.Input.ContainsKey("ParentInstanceId") &&
|
||||
opts.Input.Count == 1
|
||||
),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Use_Null_CorrelationId_When_Not_Provided()
|
||||
{
|
||||
// Arrange
|
||||
var executeWorkflow = new ExecuteWorkflow
|
||||
{
|
||||
WorkflowDefinitionId = new(DefaultWorkflowDefinitionId)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (_, workflowInvoker) = await ExecuteAsync(executeWorkflow);
|
||||
|
||||
// Assert
|
||||
await workflowInvoker.Received(1).InvokeAsync(
|
||||
Arg.Any<WorkflowGraph>(),
|
||||
Arg.Is<RunWorkflowOptions>(opts => opts.CorrelationId == null),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(WorkflowSubStatus.Cancelled)]
|
||||
[InlineData(WorkflowSubStatus.Faulted)]
|
||||
[InlineData(WorkflowSubStatus.Suspended)]
|
||||
public async Task Should_Handle_Different_Workflow_SubStatuses(WorkflowSubStatus subStatus)
|
||||
{
|
||||
// Arrange
|
||||
var executeWorkflow = new ExecuteWorkflow
|
||||
{
|
||||
WorkflowDefinitionId = new(DefaultWorkflowDefinitionId),
|
||||
WaitForCompletion = new(false)
|
||||
};
|
||||
|
||||
// Act
|
||||
var (context, _) = await ExecuteAsync(executeWorkflow, WorkflowStatus.Finished, null, subStatus);
|
||||
|
||||
// Assert
|
||||
var result = (ExecuteWorkflowResult)context.GetActivityOutput(() => executeWorkflow.Result)!;
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(subStatus, result.SubStatus);
|
||||
}
|
||||
|
||||
private static WorkflowGraph CreateMockWorkflowGraph()
|
||||
{
|
||||
var writeLine = new WriteLine("Test") { Id = "test-activity" };
|
||||
var workflow = Workflow.FromActivity(writeLine);
|
||||
workflow.Id = "test-workflow";
|
||||
var rootNode = new ActivityNode(writeLine, "Done");
|
||||
return new(workflow, rootNode, [rootNode]);
|
||||
}
|
||||
|
||||
private static IWorkflowDefinitionService CreateWorkflowDefinitionService(string workflowDefinitionId, WorkflowGraph workflowGraph)
|
||||
{
|
||||
var service = Substitute.For<IWorkflowDefinitionService>();
|
||||
service
|
||||
.FindWorkflowGraphAsync(workflowDefinitionId, Arg.Any<VersionOptions>(), Arg.Any<CancellationToken>())
|
||||
.Returns(workflowGraph);
|
||||
return service;
|
||||
}
|
||||
|
||||
private static IIdentityGenerator CreateIdentityGenerator(string generatedId)
|
||||
{
|
||||
var generator = Substitute.For<IIdentityGenerator>();
|
||||
generator.GenerateId().Returns(generatedId);
|
||||
return generator;
|
||||
}
|
||||
|
||||
private static IWorkflowInvoker CreateWorkflowInvoker(
|
||||
WorkflowStatus status,
|
||||
IDictionary<string, object>? output,
|
||||
WorkflowSubStatus subStatus = WorkflowSubStatus.Executing)
|
||||
{
|
||||
var invoker = Substitute.For<IWorkflowInvoker>();
|
||||
var workflow = Workflow.FromActivity(new WriteLine("Test"));
|
||||
var workflowState = new WorkflowState
|
||||
{
|
||||
Status = status,
|
||||
SubStatus = subStatus,
|
||||
Output = output
|
||||
};
|
||||
var workflowResult = new RunWorkflowResult(null!, workflowState, workflow, null, Journal.Empty);
|
||||
|
||||
invoker
|
||||
.InvokeAsync(Arg.Any<WorkflowGraph>(), Arg.Any<RunWorkflowOptions>(), Arg.Any<CancellationToken>())
|
||||
.Returns(workflowResult);
|
||||
|
||||
return invoker;
|
||||
}
|
||||
|
||||
private static async Task<(ActivityExecutionContext Context, IWorkflowInvoker WorkflowInvoker)>
|
||||
ExecuteAsync(
|
||||
ExecuteWorkflow executeWorkflow,
|
||||
WorkflowStatus childWorkflowStatus = WorkflowStatus.Finished,
|
||||
IDictionary<string, object>? output = null,
|
||||
WorkflowSubStatus subStatus = WorkflowSubStatus.Executing,
|
||||
IWorkflowDefinitionService? customWorkflowDefinitionService = null,
|
||||
IWorkflowInvoker? customWorkflowInvoker = null,
|
||||
IIdentityGenerator? customIdentityGenerator = null)
|
||||
{
|
||||
var workflowGraph = CreateMockWorkflowGraph();
|
||||
var workflowDefinitionService = customWorkflowDefinitionService ?? CreateWorkflowDefinitionService(DefaultWorkflowDefinitionId, workflowGraph);
|
||||
var identityGenerator = customIdentityGenerator ?? CreateIdentityGenerator(DefaultGeneratedId);
|
||||
var workflowInvoker = customWorkflowInvoker ?? CreateWorkflowInvoker(childWorkflowStatus, output, subStatus);
|
||||
|
||||
var context = await new ActivityTestFixture(executeWorkflow)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton(workflowDefinitionService);
|
||||
services.AddSingleton(workflowInvoker);
|
||||
services.AddSingleton(identityGenerator);
|
||||
services.AddSingleton<IStimulusHasher>(_ => Substitute.For<IStimulusHasher>());
|
||||
})
|
||||
.ExecuteAsync();
|
||||
|
||||
return (context, workflowInvoker);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue