Merge branch 'develop/3.6.0' into feat/unit-test-coverage-if
This commit is contained in:
commit
5ba0fbab56
|
|
@ -160,6 +160,42 @@ public async Task Should_Schedule_Child_Activity()
|
|||
}
|
||||
```
|
||||
|
||||
**Example (checking activity outcomes):**
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Should_Return_Multiple_Outcomes()
|
||||
{
|
||||
// Arrange
|
||||
var flowFork = new FlowFork
|
||||
{
|
||||
Branches = new(new[] { "Branch1", "Branch2", "Branch3" })
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await new ActivityTestFixture(flowFork).ExecuteAsync();
|
||||
|
||||
// Assert - Check all outcomes
|
||||
var outcomes = context.GetOutcomes().ToList();
|
||||
Assert.Equal(3, outcomes.Count);
|
||||
Assert.Contains("Branch1", outcomes);
|
||||
Assert.Contains("Branch2", outcomes);
|
||||
Assert.Contains("Branch3", outcomes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Return_Default_Outcome()
|
||||
{
|
||||
// Arrange
|
||||
var flowSwitch = new FlowSwitch();
|
||||
|
||||
// Act
|
||||
var context = await new ActivityTestFixture(flowSwitch).ExecuteAsync();
|
||||
|
||||
// Assert - Check single outcome
|
||||
Assert.True(context.HasOutcome("Default"));
|
||||
}
|
||||
```
|
||||
|
||||
#### **Integration tests:**
|
||||
- Place the activity inside a minimal workflow definition and run via [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs). Assert outputs/variables and that the activity integrates correctly with preceding/following activities.
|
||||
- If activity creates bookmarks or relies on scheduler semantics, integration tests should resume bookmarks via the engine APIs to validate resumption.
|
||||
|
|
@ -214,6 +250,8 @@ Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status);
|
|||
| `ActivityTestFixture.ExecuteAsync` | Execute activity and return context | Standard activity unit test execution |
|
||||
| `context.GetActivityOutput` | Get output from activity using expression selector | Asserting activity outputs in unit tests |
|
||||
| `context.HasScheduledActivity` | Check if activity is scheduled | Verifying scheduling behavior in unit tests |
|
||||
| `context.GetOutcomes` | Get all outcomes from activity execution | Asserting multiple outcomes in unit tests |
|
||||
| `context.HasOutcome` | Check if activity has specific outcome | Asserting single outcome in unit tests |
|
||||
| `IWorkflowRunner.RunAsync` | Execute workflow in-process | Integration / Component tests |
|
||||
| `RunActivityExtensions.RunActivityAsync` | Run single activity as workflow | Integration tests for single activities |
|
||||
| `PopulateRegistriesAsync` | Register types for JSON deserialization | Loading JSON workflows. <br/>Integration tests only |
|
||||
|
|
|
|||
|
|
@ -35,11 +35,15 @@ public static class RunWorkflowExtensions
|
|||
{
|
||||
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
|
||||
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, versionOptions ?? VersionOptions.Published);
|
||||
|
||||
if (workflowGraph == null)
|
||||
throw new InvalidOperationException($"Workflow definition with ID '{workflowDefinitionId}' not found.");
|
||||
|
||||
var workflowRuntime = services.GetRequiredService<IWorkflowRuntime>();
|
||||
var workflowClient = await workflowRuntime.CreateClientAsync();
|
||||
var response = await workflowClient.CreateAndRunInstanceAsync(new()
|
||||
{
|
||||
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowGraph!.Workflow.Identity.Id),
|
||||
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowGraph.Workflow.Identity.Id),
|
||||
Input = input
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,4 +17,25 @@ public static class ActivityExecutionContextExtensions
|
|||
{
|
||||
return activityExecutionContext.WorkflowExecutionContext.Scheduler.Find(x => x.Activity == activity) != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the collection of outcomes recorded in the execution context's journal data.
|
||||
/// </summary>
|
||||
/// <param name="activityExecutionContext">The activity execution context from which to retrieve the outcomes.</param>
|
||||
/// <returns>A collection of outcome strings, or an empty collection if no outcomes are present.</returns>
|
||||
public static IEnumerable<string> GetOutcomes(this ActivityExecutionContext activityExecutionContext)
|
||||
{
|
||||
return activityExecutionContext.JournalData.TryGetValue("Outcomes", out var outcomes) && outcomes is string[] arr ? arr : [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified outcome exists in the current activity execution context.
|
||||
/// </summary>
|
||||
/// <param name="activityExecutionContext">The activity execution context to check for the outcome.</param>
|
||||
/// <param name="outcome">The outcome to verify within the execution context.</param>
|
||||
/// <returns>True if the specified outcome exists; otherwise, false.</returns>
|
||||
public static bool HasOutcome(this ActivityExecutionContext activityExecutionContext, string outcome)
|
||||
{
|
||||
return activityExecutionContext.GetOutcomes().Contains(outcome);
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@ public class ActivityTestFixture
|
|||
// Set up variables and inputs, then execute the activity
|
||||
await SetupExistingVariablesAsync(Activity, context);
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
context.TransitionTo(ActivityStatus.Running);
|
||||
await Activity.ExecuteAsync(context);
|
||||
|
||||
return context;
|
||||
|
|
@ -167,4 +168,4 @@ public class ActivityTestFixture
|
|||
services.AddSingleton<IWorkflowExecutionContextSchedulerStrategy, FakeWorkflowExecutionContextSchedulerStrategy>();
|
||||
services.AddSingleton<IActivityExecutionContextSchedulerStrategy, FakeActivityExecutionContextSchedulerStrategy>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,6 +75,6 @@ public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions<JintOp
|
|||
|
||||
await foreach (var activityOutput in activityOutputs)
|
||||
foreach (var outputName in activityOutput.OutputNames.FilterInvalidVariableNames())
|
||||
engine.SetValue($"get{outputName}From{activityOutput.ActivityName}", (Func<object?>)(() => context.GetOutput(activityOutput.ActivityId, outputName)));
|
||||
engine.SetValue($"get{outputName}From{activityOutput.ActivityName.Pascalize()}", (Func<object?>)(() => context.GetOutput(activityOutput.ActivityId, outputName)));
|
||||
}
|
||||
}
|
||||
|
|
@ -35,5 +35,32 @@
|
|||
<None Update="Scenarios\JoinBehaviors\Workflows\decision-merge-join-none.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-with-input.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-with-input.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-with-outcomes.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-with-output.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-version-fallback.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-with-io.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-with-outcomes.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\parent-with-output.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowDefinitionActivities\Workflows\child-published-version.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@ public class Tests
|
|||
}
|
||||
|
||||
[Fact(DisplayName = "Workflows provided from JSON files that depend on other workflows are executed correctly.")]
|
||||
public async Task Test1()
|
||||
public async Task Should_Execute_Multi_Level_Nested_Workflow_Definitions()
|
||||
{
|
||||
// Populate registries.
|
||||
await _services.PopulateRegistriesAsync();
|
||||
|
||||
// Run the "matter" workflow.
|
||||
// Run the "matter" workflow which contains MoleculeActivity, which contains AtomActivity.
|
||||
// Matter -> 3x Molecule -> each Molecule contains 3x Atom = 9 "Atom" outputs total
|
||||
await _services.RunWorkflowUntilEndAsync("matter-workflow");
|
||||
|
||||
// Assert.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionActivities;
|
||||
|
||||
public class Tests
|
||||
{
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public Tests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_services = new TestApplicationBuilder(testOutputHelper)
|
||||
.WithCapturingTextWriter(_capturingTextWriter)
|
||||
.WithWorkflowsFromDirectory("Scenarios", "WorkflowDefinitionActivities", "Workflows")
|
||||
.Build();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WorkflowDefinitionActivityTestCases))]
|
||||
public async Task Should_Execute_WorkflowDefinitionActivity_Scenarios(string workflowDefinitionId, string expectedOutput)
|
||||
{
|
||||
// Populate registries.
|
||||
await _services.PopulateRegistriesAsync();
|
||||
|
||||
// Run the workflow.
|
||||
await _services.RunWorkflowUntilEndAsync(workflowDefinitionId);
|
||||
|
||||
// Assert - verify expected output appears.
|
||||
var lines = _capturingTextWriter.Lines.ToList();
|
||||
Assert.Contains(expectedOutput, lines);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> WorkflowDefinitionActivityTestCases()
|
||||
{
|
||||
yield return ["parent-with-input", "Hello from parent!"];
|
||||
yield return ["parent-with-output", "Received from child: Child output value"];
|
||||
yield return ["parent-with-outcomes", "Success path taken"];
|
||||
yield return ["parent-version-fallback", "Published version executed"];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"id": "child-published-v1",
|
||||
"definitionId": "child-published",
|
||||
"name": "ChildPublished",
|
||||
"version": 1,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"usableAsActivity": true,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"root": {
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"id": "WriteLine1",
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Published version executed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "e694d38a6d17f6ab",
|
||||
"definitionId": "d0ae88e4c15eacc0",
|
||||
"name": "ChildWithInput",
|
||||
"createdAt": "2025-10-21T12:06:12.52497\u002B00:00",
|
||||
"version": 1,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [],
|
||||
"inputs": [
|
||||
{
|
||||
"uiHint": "singleline",
|
||||
"storageDriverType": "Elsa.Workflows.WorkflowInstanceStorageDriver, Elsa.Workflows.Core",
|
||||
"type": "String",
|
||||
"name": "Message",
|
||||
"displayName": "Message",
|
||||
"description": "The message to write",
|
||||
"category": "Primitives"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"usableAsActivity": true,
|
||||
"autoUpdateConsumingWorkflows": true
|
||||
},
|
||||
"root": {
|
||||
"id": "855226c76e9ad551",
|
||||
"nodeId": "Workflow1:855226c76e9ad551",
|
||||
"name": "Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "JavaScript",
|
||||
"value": "getMessage()"
|
||||
}
|
||||
},
|
||||
"id": "eff084a902159510",
|
||||
"nodeId": "Workflow1:855226c76e9ad551:eff084a902159510",
|
||||
"name": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -278.5,
|
||||
"y": -178.5
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "85761829037332b7",
|
||||
"definitionId": "7ee5c04453217e3f",
|
||||
"name": "ChildWithOutcomes",
|
||||
"createdAt": "2025-10-21T13:11:59.899105\u002B00:00",
|
||||
"version": 2,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [
|
||||
"Success",
|
||||
"Failed"
|
||||
],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"usableAsActivity": true,
|
||||
"autoUpdateConsumingWorkflows": true
|
||||
},
|
||||
"root": {
|
||||
"id": "3519519e32e50f38",
|
||||
"nodeId": "Workflow1:3519519e32e50f38",
|
||||
"name": "Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"outcomes": {
|
||||
"typeName": "Object",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Success"
|
||||
}
|
||||
},
|
||||
"id": "a093b294de68dc32",
|
||||
"nodeId": "Workflow1:3519519e32e50f38:a093b294de68dc32",
|
||||
"name": "Complete1",
|
||||
"type": "Elsa.Complete",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -160.5,
|
||||
"y": -143.5
|
||||
},
|
||||
"size": {
|
||||
"width": 157.15625,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "4da8180d0c9f7e3f",
|
||||
"definitionId": "5781ed56ea4b160",
|
||||
"name": "ChildWithOutput",
|
||||
"createdAt": "2025-10-21T12:15:08.404102\u002B00:00",
|
||||
"version": 4,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"type": "String",
|
||||
"name": "Output",
|
||||
"displayName": "Output",
|
||||
"description": "The result value",
|
||||
"category": "Primitives"
|
||||
}
|
||||
],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"usableAsActivity": true,
|
||||
"autoUpdateConsumingWorkflows": true
|
||||
},
|
||||
"root": {
|
||||
"id": "14493561a73301af",
|
||||
"nodeId": "Workflow1:14493561a73301af",
|
||||
"name": "Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"outputName": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Output"
|
||||
}
|
||||
},
|
||||
"outputValue": {
|
||||
"typeName": "Object",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Child output value"
|
||||
}
|
||||
},
|
||||
"id": "461b5cebad79475c",
|
||||
"nodeId": "Workflow1:14493561a73301af:461b5cebad79475c",
|
||||
"name": "SetOutput1",
|
||||
"type": "Elsa.SetOutput",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -353.5,
|
||||
"y": -235.5
|
||||
},
|
||||
"size": {
|
||||
"width": 132.3203125,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"id": "parent-version-fallback-v1",
|
||||
"definitionId": "parent-version-fallback",
|
||||
"name": "ParentVersionFallback",
|
||||
"version": 1,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"id": "Flowchart1",
|
||||
"start": "ChildActivity1",
|
||||
"activities": [
|
||||
{
|
||||
"type": "ChildPublished",
|
||||
"version": 1,
|
||||
"id": "ChildActivity1",
|
||||
"workflowDefinitionId": "child-published",
|
||||
"workflowDefinitionVersionId": "non-existent-version-id"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "70a093d85f846d05",
|
||||
"definitionId": "parent-with-input",
|
||||
"name": "ParentWithInput",
|
||||
"createdAt": "2025-10-21T12:08:40.940992\u002B00:00",
|
||||
"version": 4,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"id": "Flowchart1",
|
||||
"nodeId": "Workflow2:Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"workflowDefinitionId": "d0ae88e4c15eacc0",
|
||||
"workflowDefinitionVersionId": "e694d38a6d17f6ab",
|
||||
"latestAvailablePublishedVersion": 1,
|
||||
"latestAvailablePublishedVersionId": "e694d38a6d17f6ab",
|
||||
"id": "ChildActivity1",
|
||||
"nodeId": "Workflow2:Flowchart1:ChildActivity1",
|
||||
"name": null,
|
||||
"type": "ChildWithInput",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -80,
|
||||
"y": -40
|
||||
},
|
||||
"size": {
|
||||
"width": 161.8515625,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Hello from parent!"
|
||||
},
|
||||
"memoryReference": {
|
||||
"id": "ChildActivity1:input-message"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"id": "parent-with-io-v1",
|
||||
"definitionId": "parent-with-io",
|
||||
"name": "ParentWithIO",
|
||||
"version": 1,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"root": {
|
||||
"type": "ChildWithInput",
|
||||
"version": 1,
|
||||
"id": "ChildActivity1",
|
||||
"workflowDefinitionId": "child-with-input",
|
||||
"workflowDefinitionVersionId": "child-with-input-v1",
|
||||
"message": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Test message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "a09fb3bdfd316332",
|
||||
"definitionId": "parent-with-outcomes",
|
||||
"name": "ParentWithOutcomes",
|
||||
"createdAt": "2025-10-21T13:11:28.283567\u002B00:00",
|
||||
"version": 2,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"id": "Flowchart1",
|
||||
"nodeId": "Workflow2:Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"workflowDefinitionId": "7ee5c04453217e3f",
|
||||
"workflowDefinitionVersionId": "85761829037332b7",
|
||||
"latestAvailablePublishedVersion": 2,
|
||||
"latestAvailablePublishedVersionId": "85761829037332b7",
|
||||
"id": "ChildActivity1",
|
||||
"nodeId": "Workflow2:Flowchart1:ChildActivity1",
|
||||
"name": null,
|
||||
"type": "ChildWithOutcomes",
|
||||
"version": 2,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -140,
|
||||
"y": 0
|
||||
},
|
||||
"size": {
|
||||
"width": 198.7421875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Failed path taken"
|
||||
}
|
||||
},
|
||||
"id": "WriteLineFailed",
|
||||
"nodeId": "Workflow2:Flowchart1:WriteLineFailed",
|
||||
"name": null,
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 140,
|
||||
"y": 80
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "Literal",
|
||||
"value": "Success path taken"
|
||||
}
|
||||
},
|
||||
"id": "WriteLineSuccess",
|
||||
"nodeId": "Workflow2:Flowchart1:WriteLineSuccess",
|
||||
"name": null,
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": 180,
|
||||
"y": -220
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": [
|
||||
{
|
||||
"source": {
|
||||
"activity": "ChildActivity1",
|
||||
"port": "Success"
|
||||
},
|
||||
"target": {
|
||||
"activity": "WriteLineSuccess",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
},
|
||||
{
|
||||
"source": {
|
||||
"activity": "ChildActivity1",
|
||||
"port": "Failed"
|
||||
},
|
||||
"target": {
|
||||
"activity": "WriteLineFailed",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
{
|
||||
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
|
||||
"id": "b1f4fc14b3c6b51",
|
||||
"definitionId": "parent-with-output",
|
||||
"name": "ParentWithOutput",
|
||||
"createdAt": "2025-10-21T12:27:50.918094\u002B00:00",
|
||||
"version": 1,
|
||||
"toolVersion": "3.6.0.0",
|
||||
"variables": [],
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"outcomes": [],
|
||||
"customProperties": {},
|
||||
"isReadonly": false,
|
||||
"isSystem": false,
|
||||
"isLatest": true,
|
||||
"isPublished": true,
|
||||
"options": {
|
||||
"autoUpdateConsumingWorkflows": false
|
||||
},
|
||||
"root": {
|
||||
"id": "bcbe1e9e5a9c9b0e",
|
||||
"nodeId": "Workflow2:bcbe1e9e5a9c9b0e",
|
||||
"name": "Flowchart1",
|
||||
"type": "Elsa.Flowchart",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"notFoundConnections": [],
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {},
|
||||
"activities": [
|
||||
{
|
||||
"workflowDefinitionId": "5781ed56ea4b160",
|
||||
"workflowDefinitionVersionId": "4da8180d0c9f7e3f",
|
||||
"latestAvailablePublishedVersion": 4,
|
||||
"latestAvailablePublishedVersionId": "4da8180d0c9f7e3f",
|
||||
"id": "c98383464dab2047",
|
||||
"nodeId": "Workflow2:bcbe1e9e5a9c9b0e:c98383464dab2047",
|
||||
"name": "ChildWithOutput1",
|
||||
"type": "ChildWithOutput",
|
||||
"version": 4,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -323.5,
|
||||
"y": -85.5
|
||||
},
|
||||
"size": {
|
||||
"width": 174.1640625,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"text": {
|
||||
"typeName": "String",
|
||||
"expression": {
|
||||
"type": "JavaScript",
|
||||
"value": "\u0060Received from child: ${getOutputFromChildWithOutput1()}\u0060"
|
||||
}
|
||||
},
|
||||
"id": "b4f314dce11c7135",
|
||||
"nodeId": "Workflow2:bcbe1e9e5a9c9b0e:b4f314dce11c7135",
|
||||
"name": "WriteLine1",
|
||||
"type": "Elsa.WriteLine",
|
||||
"version": 1,
|
||||
"customProperties": {
|
||||
"canStartWorkflow": false,
|
||||
"runAsynchronously": false
|
||||
},
|
||||
"metadata": {
|
||||
"designer": {
|
||||
"position": {
|
||||
"x": -27.5,
|
||||
"y": -85.5
|
||||
},
|
||||
"size": {
|
||||
"width": 159.6171875,
|
||||
"height": 67.9765625
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"connections": [
|
||||
{
|
||||
"source": {
|
||||
"activity": "c98383464dab2047",
|
||||
"port": "Done"
|
||||
},
|
||||
"target": {
|
||||
"activity": "b4f314dce11c7135",
|
||||
"port": "In"
|
||||
},
|
||||
"vertices": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Branching;
|
||||
|
||||
public class FlowForkTests
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(BranchTestCases))]
|
||||
public async Task Should_Complete_With_Specified_Branches(string[] branches, string[] expectedOutcomes)
|
||||
{
|
||||
// Arrange
|
||||
var flowFork = new FlowFork();
|
||||
if (branches.Length > 0)
|
||||
flowFork.Branches = new(branches);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(flowFork);
|
||||
|
||||
// Assert - Activity should complete with all outcomes.
|
||||
var outcomes = context.GetOutcomes().ToList();
|
||||
|
||||
Assert.Equal(expectedOutcomes.Length, outcomes.Count);
|
||||
foreach (var expectedOutcome in expectedOutcomes)
|
||||
Assert.Contains(expectedOutcome, outcomes);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> BranchTestCases()
|
||||
{
|
||||
yield return [Array.Empty<string>(), new[] { "Done" }];
|
||||
yield return [new[] { "SingleBranch" }, new[] { "SingleBranch" }];
|
||||
yield return [new[] { "Branch1", "Branch2", "Branch3" }, new[] { "Branch1", "Branch2", "Branch3" }];
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
|
||||
{
|
||||
return new ActivityTestFixture(activity).ExecuteAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
using Elsa.Expressions.Models;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities.Flowchart.Activities;
|
||||
using Elsa.Workflows.Activities.Flowchart.Models;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Branching;
|
||||
|
||||
public class FlowSwitchTests
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(DefaultOutcomeTestCases))]
|
||||
public async Task Should_Return_Default_When_No_Cases_Match(List<FlowSwitchCase> cases)
|
||||
{
|
||||
// Arrange
|
||||
var flowSwitch = new FlowSwitch { Cases = cases };
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(flowSwitch);
|
||||
|
||||
// Assert - Activity should return Default outcome when no cases match.
|
||||
Assert.True(context.HasOutcome("Default"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SwitchModeTestCases))]
|
||||
public async Task Should_Return_Outcomes_Based_On_Switch_Mode(
|
||||
List<FlowSwitchCase> cases,
|
||||
SwitchMode mode,
|
||||
string[] expectedOutcomes)
|
||||
{
|
||||
// Arrange
|
||||
var flowSwitch = new FlowSwitch
|
||||
{
|
||||
Cases = cases,
|
||||
Mode = new(mode)
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(flowSwitch);
|
||||
|
||||
// Assert - Activity should return outcomes based on switch mode.
|
||||
var outcomes = context.GetOutcomes().ToList();
|
||||
Assert.Equal(expectedOutcomes.Length, outcomes.Count);
|
||||
foreach (var expectedOutcome in expectedOutcomes)
|
||||
Assert.Contains(expectedOutcome, outcomes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Evaluate_Cases_With_Literal_Expression()
|
||||
{
|
||||
// Arrange
|
||||
var flowSwitch = new FlowSwitch
|
||||
{
|
||||
Cases = new List<FlowSwitchCase>
|
||||
{
|
||||
new("TrueCase", Expression.LiteralExpression(true)),
|
||||
new("FalseCase", Expression.LiteralExpression(false))
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(flowSwitch);
|
||||
|
||||
// Assert - Activity should match the case with true literal.
|
||||
Assert.True(context.HasOutcome("TrueCase"));
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> DefaultOutcomeTestCases()
|
||||
{
|
||||
yield return [new List<FlowSwitchCase>()];
|
||||
yield return
|
||||
[
|
||||
new List<FlowSwitchCase>
|
||||
{
|
||||
new("Case1", () => false),
|
||||
new("Case2", () => false)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> SwitchModeTestCases()
|
||||
{
|
||||
var cases = new List<FlowSwitchCase>
|
||||
{
|
||||
new("Case1", () => false),
|
||||
new("Case2", () => true),
|
||||
new("Case3", () => true)
|
||||
};
|
||||
|
||||
yield return [cases, SwitchMode.MatchFirst, new[] { "Case2" }];
|
||||
yield return [cases, SwitchMode.MatchAny, new[] { "Case2", "Case3" }];
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
|
||||
{
|
||||
return new ActivityTestFixture(activity).ExecuteAsync();
|
||||
}
|
||||
}
|
||||
194
test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs
Normal file
194
test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
using Elsa.Expressions.Models;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Branching;
|
||||
|
||||
public class SwitchTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(SwitchMode.MatchFirst, true)]
|
||||
[InlineData(SwitchMode.MatchAny, true)]
|
||||
[InlineData(SwitchMode.MatchFirst, false)]
|
||||
[InlineData(SwitchMode.MatchAny, false)]
|
||||
public async Task Should_Handle_No_Matching_Cases_Correctly(SwitchMode mode, bool hasDefault)
|
||||
{
|
||||
// Arrange
|
||||
var defaultActivity = hasDefault ? Substitute.For<IActivity>() : null;
|
||||
var switchActivity = new Switch
|
||||
{
|
||||
Mode = new(mode),
|
||||
Cases = new List<SwitchCase>
|
||||
{
|
||||
new("False Case", Expression.LiteralExpression(false), Substitute.For<IActivity>())
|
||||
},
|
||||
Default = defaultActivity
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(switchActivity);
|
||||
|
||||
// Assert
|
||||
var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
if (hasDefault)
|
||||
{
|
||||
Assert.Single(scheduledActivities);
|
||||
Assert.Equal(defaultActivity, scheduledActivities.First().Activity);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Empty(scheduledActivities);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SwitchMode.MatchFirst, 1)]
|
||||
[InlineData(SwitchMode.MatchAny, 2)]
|
||||
public async Task Should_Handle_Multiple_Matching_Cases_According_To_Mode(SwitchMode mode, int expectedScheduledCount)
|
||||
{
|
||||
// Arrange
|
||||
var firstTrueActivity = Substitute.For<IActivity>();
|
||||
var secondTrueActivity = Substitute.For<IActivity>();
|
||||
var switchActivity = new Switch
|
||||
{
|
||||
Mode = new(mode),
|
||||
Cases = new List<SwitchCase>
|
||||
{
|
||||
new("False", Expression.LiteralExpression(false), Substitute.For<IActivity>()),
|
||||
new("First True", Expression.LiteralExpression(true), firstTrueActivity),
|
||||
new("Second True", Expression.LiteralExpression(true), secondTrueActivity)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(switchActivity);
|
||||
|
||||
// Assert
|
||||
var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
Assert.Equal(expectedScheduledCount, scheduledActivities.Count);
|
||||
|
||||
if (mode == SwitchMode.MatchFirst)
|
||||
{
|
||||
Assert.Equal(firstTrueActivity, scheduledActivities.First().Activity);
|
||||
}
|
||||
else // MatchAny
|
||||
{
|
||||
Assert.Contains(scheduledActivities, s => s.Activity == firstTrueActivity);
|
||||
Assert.Contains(scheduledActivities, s => s.Activity == secondTrueActivity);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Use_MatchFirst_As_Default_Mode()
|
||||
{
|
||||
// Arrange
|
||||
var firstTrueActivity = Substitute.For<IActivity>();
|
||||
var secondTrueActivity = Substitute.For<IActivity>();
|
||||
var switchActivity = new Switch
|
||||
{
|
||||
// No Mode explicitly set - should default to MatchFirst
|
||||
Cases = new List<SwitchCase>
|
||||
{
|
||||
new("First True", Expression.LiteralExpression(true), firstTrueActivity),
|
||||
new("Second True", Expression.LiteralExpression(true), secondTrueActivity)
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(switchActivity);
|
||||
|
||||
// Assert
|
||||
var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
Assert.Single(scheduledActivities);
|
||||
Assert.Equal(firstTrueActivity, scheduledActivities.First().Activity);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SwitchMode.MatchFirst)]
|
||||
[InlineData(SwitchMode.MatchAny)]
|
||||
public async Task Should_Schedule_Default_When_Null_Case_Condition_Evaluates_False(SwitchMode mode)
|
||||
{
|
||||
// Arrange
|
||||
var defaultActivity = Substitute.For<IActivity>();
|
||||
var switchActivity = new Switch
|
||||
{
|
||||
Mode = new(mode),
|
||||
Cases = new List<SwitchCase>
|
||||
{
|
||||
new("Null condition", Expression.LiteralExpression(null), Substitute.For<IActivity>())
|
||||
},
|
||||
Default = defaultActivity
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(switchActivity);
|
||||
|
||||
// Assert
|
||||
var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
Assert.Single(scheduledActivities);
|
||||
Assert.Equal(defaultActivity, scheduledActivities.First().Activity);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SwitchMode.MatchFirst, true)]
|
||||
[InlineData(SwitchMode.MatchAny, true)]
|
||||
[InlineData(SwitchMode.MatchFirst, false)]
|
||||
[InlineData(SwitchMode.MatchAny, false)]
|
||||
public async Task Should_Handle_Empty_Cases_Correctly(SwitchMode mode, bool hasDefault)
|
||||
{
|
||||
// Arrange
|
||||
var defaultActivity = hasDefault ? Substitute.For<IActivity>() : null;
|
||||
var switchActivity = new Switch
|
||||
{
|
||||
Mode = new(mode),
|
||||
Cases = new List<SwitchCase>(),
|
||||
Default = defaultActivity
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ExecuteAsync(switchActivity);
|
||||
|
||||
// Assert
|
||||
var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
if (hasDefault)
|
||||
{
|
||||
Assert.Single(scheduledActivities);
|
||||
Assert.Equal(defaultActivity, scheduledActivities.First().Activity);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Empty(scheduledActivities);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Initialize_Cases_Collection_By_Default()
|
||||
{
|
||||
// Arrange & Act
|
||||
var switchActivity = new Switch();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(switchActivity.Cases);
|
||||
Assert.Empty(switchActivity.Cases);
|
||||
|
||||
switchActivity.Cases.Add(new("Test", Expression.LiteralExpression(true), Substitute.For<IActivity>()));
|
||||
Assert.Single(switchActivity.Cases);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Initialize_Mode_To_MatchFirst_By_Default()
|
||||
{
|
||||
// Arrange
|
||||
var switchActivity = new Switch();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(switchActivity.Mode);
|
||||
// The actual default value verification is handled by the mode-specific behavior tests
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
|
||||
{
|
||||
return new ActivityTestFixture(activity).ExecuteAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
using System.Net;
|
||||
using Elsa.Activities.UnitTests.Http.Helpers;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Http;
|
||||
|
||||
public class FlowSendHttpRequestTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("GET", "https://api.example.com/data", "{\"result\": \"success\"}", 200)]
|
||||
[InlineData("POST", "https://api.example.com/create", "{\"id\": 123}", 201)]
|
||||
[InlineData("PUT", "https://api.example.com/update", "{\"updated\": true}", 200)]
|
||||
public async Task Should_Send_Request_And_Set_Status_Code_Output(string method, string url, string jsonResponse, int expectedStatusCode)
|
||||
{
|
||||
// Arrange
|
||||
var expectedUrl = new Uri(url);
|
||||
var expectedMethod = new HttpMethod(method);
|
||||
var expectedHttpStatusCode = (HttpStatusCode)expectedStatusCode;
|
||||
var requestCapture = new SendHttpRequestTestHelpers.RequestCapture();
|
||||
var responseHandler = SendHttpRequestTestHelpers.CreateResponseHandler(expectedHttpStatusCode, jsonResponse, requestCapture);
|
||||
var flowSendHttpRequest = CreateFlowSendHttpRequest(expectedUrl, method);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(flowSendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(requestCapture.CapturedRequest);
|
||||
Assert.Equal(expectedMethod, requestCapture.CapturedRequest.Method);
|
||||
Assert.Equal(expectedUrl, requestCapture.CapturedRequest.RequestUri);
|
||||
|
||||
var statusCodeOutput = context.GetActivityOutput(() => flowSendHttpRequest.StatusCode);
|
||||
Assert.Equal(expectedStatusCode, statusCodeOutput);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Bearer token123")]
|
||||
[InlineData("Basic YWRtaW46cGFzcw==")]
|
||||
[InlineData("ApiKey abc123")]
|
||||
public async Task Should_Add_Authorization_Header(string authorizationHeader)
|
||||
{
|
||||
// Arrange
|
||||
var expectedUrl = new Uri("https://api.example.com/secure");
|
||||
|
||||
var requestCapture = new SendHttpRequestTestHelpers.RequestCapture();
|
||||
var responseHandler = SendHttpRequestTestHelpers.CreateResponseHandler(HttpStatusCode.OK, null, requestCapture);
|
||||
var flowSendHttpRequest = CreateFlowSendHttpRequest(expectedUrl, authorization: authorizationHeader);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(flowSendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(requestCapture.CapturedRequest);
|
||||
Assert.NotNull(requestCapture.CapturedRequest.Headers.Authorization);
|
||||
Assert.Equal(authorizationHeader, requestCapture.CapturedRequest.Headers.Authorization.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(StatusCodeOutcomeTestCases))]
|
||||
public async Task Should_Return_Outcome_Based_On_Status_Code(
|
||||
int[] expectedStatusCodes,
|
||||
HttpStatusCode actualStatusCode,
|
||||
string[] expectedOutcomes)
|
||||
{
|
||||
// Arrange
|
||||
var flowSendHttpRequest = CreateFlowSendHttpRequest(
|
||||
new("https://api.example.com/test"),
|
||||
expectedStatusCodes: expectedStatusCodes);
|
||||
|
||||
var responseHandler = SendHttpRequestTestHelpers.CreateResponseHandler(actualStatusCode);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(flowSendHttpRequest, responseHandler);
|
||||
|
||||
// Assert - Activity should return outcomes based on status code match.
|
||||
var outcomes = context.GetOutcomes().ToList();
|
||||
Assert.Equal(expectedOutcomes.Length, outcomes.Count);
|
||||
foreach (var expectedOutcome in expectedOutcomes)
|
||||
Assert.Contains(expectedOutcome, outcomes);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> StatusCodeOutcomeTestCases()
|
||||
{
|
||||
// Expected status codes: 200, 404
|
||||
// Status code matches - returns status code + Done
|
||||
yield return [new[] { 200, 404 }, HttpStatusCode.OK, new[] { "200", "Done" }];
|
||||
yield return [new[] { 200, 404 }, HttpStatusCode.NotFound, new[] { "404", "Done" }];
|
||||
|
||||
// Status code doesn't match - returns "Unmatched status code" + Done
|
||||
yield return [new[] { 200, 404 }, HttpStatusCode.InternalServerError, new[] { "Unmatched status code", "Done" }];
|
||||
|
||||
// No expected status codes - returns only Done
|
||||
yield return [Array.Empty<int>(), HttpStatusCode.OK, new[] { "Done" }];
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Return_FailedToConnect_Outcome_On_HttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var flowSendHttpRequest = CreateFlowSendHttpRequest(new("https://api.example.com/error"));
|
||||
var responseHandler = SendHttpRequestTestHelpers.CreateExceptionHandler<HttpRequestException>("Connection failed");
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(flowSendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasOutcome("Failed to connect"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Return_Timeout_Outcome_On_TaskCanceledException()
|
||||
{
|
||||
// Arrange
|
||||
var flowSendHttpRequest = CreateFlowSendHttpRequest(new("https://api.example.com/timeout"));
|
||||
var responseHandler = SendHttpRequestTestHelpers.CreateExceptionHandler<TaskCanceledException>("Request timed out");
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(flowSendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasOutcome("Timeout"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Set_Response_Headers_Output()
|
||||
{
|
||||
// Arrange
|
||||
var expectedHeaders = new Dictionary<string, string>
|
||||
{
|
||||
{ "Custom-Header", "CustomValue" },
|
||||
{ "X-Rate-Limit", "100" }
|
||||
};
|
||||
|
||||
var responseHandler = SendHttpRequestTestHelpers.CreateResponseHandler(HttpStatusCode.OK, additionalHeaders: expectedHeaders);
|
||||
var flowSendHttpRequest = CreateFlowSendHttpRequest(new("https://api.example.com/headers"));
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(flowSendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
var responseHeadersObj = context.GetActivityOutput(() => flowSendHttpRequest.ResponseHeaders);
|
||||
var responseHeaders = responseHeadersObj as HttpHeaders;
|
||||
Assert.NotNull(responseHeaders);
|
||||
Assert.True(responseHeaders.ContainsKey("Custom-Header"));
|
||||
Assert.True(responseHeaders.ContainsKey("X-Rate-Limit"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Have_Correct_Activity_Attributes()
|
||||
{
|
||||
var fixture = new ActivityTestFixture(new FlowSendHttpRequest());
|
||||
fixture.AssertActivityAttributes(
|
||||
expectedNamespace: "Elsa",
|
||||
expectedCategory: "HTTP",
|
||||
expectedDisplayName: "HTTP Request (flow)",
|
||||
expectedDescription: "Send an HTTP request.",
|
||||
expectedKind: ActivityKind.Task
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Have_Default_Expected_Status_Codes()
|
||||
{
|
||||
// Arrange
|
||||
var flowSendHttpRequest = new FlowSendHttpRequest();
|
||||
|
||||
// Act - Get the default value
|
||||
var defaultValue = ((IActivityPropertyDefaultValueProvider)flowSendHttpRequest)
|
||||
.GetDefaultValue(typeof(FlowSendHttpRequest).GetProperty(nameof(FlowSendHttpRequest.ExpectedStatusCodes))!);
|
||||
|
||||
// Assert
|
||||
var defaultStatusCodes = defaultValue as List<int>;
|
||||
Assert.NotNull(defaultStatusCodes);
|
||||
Assert.Single(defaultStatusCodes);
|
||||
Assert.Equal(200, defaultStatusCodes.First());
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
private static FlowSendHttpRequest CreateFlowSendHttpRequest(
|
||||
Uri url,
|
||||
string method = "GET",
|
||||
object? content = null,
|
||||
string? contentType = null,
|
||||
string? authorization = null,
|
||||
int[]? expectedStatusCodes = null)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Url = new(url),
|
||||
Method = new(method),
|
||||
Content = content != null ? new Input<object?>(content) : null!,
|
||||
ContentType = contentType != null ? new Input<string?>(contentType) : null!,
|
||||
Authorization = authorization != null ? new Input<string?>(authorization) : null!,
|
||||
ExpectedStatusCodes = expectedStatusCodes != null ? new Input<ICollection<int>>(expectedStatusCodes) : null!
|
||||
};
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteActivityAsync(
|
||||
FlowSendHttpRequest flowSendHttpRequest,
|
||||
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responseHandler)
|
||||
{
|
||||
return new ActivityTestFixture(flowSendHttpRequest).WithHttpServices(responseHandler).ExecuteAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ using Elsa.Workflows;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Helpers;
|
||||
namespace Elsa.Activities.UnitTests.Http.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring HTTP-related services in ActivityTestFixture.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
using System.Net;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Http.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper methods for testing SendHttpRequest and FlowSendHttpRequest activities.
|
||||
/// </summary>
|
||||
public static class SendHttpRequestTestHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a response handler that returns a specific HTTP status code and optional content.
|
||||
/// </summary>
|
||||
public static Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> CreateResponseHandler(
|
||||
HttpStatusCode statusCode,
|
||||
string? content = null,
|
||||
RequestCapture? requestCapture = null,
|
||||
Dictionary<string, string>? additionalHeaders = null)
|
||||
{
|
||||
return (request, _) =>
|
||||
{
|
||||
if (requestCapture != null)
|
||||
requestCapture.CapturedRequest = request;
|
||||
return Task.FromResult(ActivityTestFixtureHttpExtensions.CreateHttpResponse(statusCode, content, additionalHeaders));
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an exception handler that throws a specific exception type with a message.
|
||||
/// </summary>
|
||||
public static Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> CreateExceptionHandler<TException>(string message)
|
||||
where TException : Exception
|
||||
{
|
||||
return (_, _) => throw ((TException)Activator.CreateInstance(typeof(TException), message)!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures HTTP request details during test execution.
|
||||
/// </summary>
|
||||
public sealed class RequestCapture
|
||||
{
|
||||
public HttpRequestMessage? CapturedRequest { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Elsa.Activities.UnitTests.Helpers;
|
||||
namespace Elsa.Activities.UnitTests.Http.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Custom test HTTP message handler that allows full control over HTTP responses for testing.
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using Elsa.Activities.UnitTests.Helpers;
|
||||
using Elsa.Activities.UnitTests.Http.Helpers;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http;
|
||||
using Elsa.Testing.Shared;
|
||||
|
|
|
|||
Loading…
Reference in a new issue