From 234a2a80c0ecf1c01f21ed3e998615c16a735a7b Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 14 Oct 2025 10:04:24 +0200 Subject: [PATCH 01/10] Unit test coverage for Switch activity --- .../Branching/SwitchTests.cs | 349 ++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs new file mode 100644 index 000000000..6d728866d --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs @@ -0,0 +1,349 @@ +using Elsa.Activities.UnitTests.Helpers; +using Elsa.Expressions.Models; +using Elsa.Workflows; +using NSubstitute; + +namespace Elsa.Activities.UnitTests.Branching; + +public class SwitchTests +{ + [Fact] + public async Task Should_Execute_Activity_Without_Errors() + { + // Arrange + var switchActivity = new Switch(); + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Execute_With_Default_Branch_When_No_Case_Matches() + { + // Arrange + var mockActivity = Substitute.For(); + var switchActivity = new Switch + { + Cases = new List + { + new("Case 1", Expression.LiteralExpression(false), Substitute.For()) + }, + Default = mockActivity + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Execute_With_All_Cases_False_In_MatchFirst_Mode() + { + // Arrange + var switchActivity = new Switch + { + Mode = new Input(SwitchMode.MatchFirst), + Cases = new List + { + new("Case 1", Expression.LiteralExpression(false), Substitute.For()), + new("Case 2", Expression.LiteralExpression(false), Substitute.For()) + }, + Default = Substitute.For() + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Execute_With_Single_True_Case_In_MatchFirst_Mode() + { + // Arrange + var switchActivity = new Switch + { + Mode = new Input(SwitchMode.MatchFirst), + Cases = new List + { + new("False Case", Expression.LiteralExpression(false), Substitute.For()), + new("True Case", Expression.LiteralExpression(true), Substitute.For()) + } + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Execute_With_Multiple_True_Cases_In_MatchFirst_Mode() + { + // Arrange + var switchActivity = new Switch + { + Mode = new Input(SwitchMode.MatchFirst), + Cases = new List + { + new("First True", Expression.LiteralExpression(true), Substitute.For()), + new("Second True", Expression.LiteralExpression(true), Substitute.For()) + } + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Execute_With_Multiple_True_Cases_In_MatchAny_Mode() + { + // Arrange + var switchActivity = new Switch + { + Mode = new Input(SwitchMode.MatchAny), + Cases = new List + { + new("First True", Expression.LiteralExpression(true), Substitute.For()), + new("False", Expression.LiteralExpression(false), Substitute.For()), + new("Second True", Expression.LiteralExpression(true), Substitute.For()) + } + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Execute_With_All_Cases_False_In_MatchAny_Mode() + { + // Arrange + var switchActivity = new Switch + { + Mode = new Input(SwitchMode.MatchAny), + Cases = new List + { + new("Case 1", Expression.LiteralExpression(false), Substitute.For()), + new("Case 2", Expression.LiteralExpression(false), Substitute.For()) + }, + Default = Substitute.For() + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Execute_When_Default_Branch_Is_Absent_And_No_Matches() + { + // Arrange + var switchActivity = new Switch + { + Cases = new List + { + new("Case 1", Expression.LiteralExpression(false), Substitute.For()) + }, + Default = null // No default branch + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Handle_Empty_Case_Collection() + { + // Arrange + var switchActivity = new Switch + { + Cases = new List(), // Empty collection + Default = Substitute.For() + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Handle_Empty_Case_Collection_Without_Default() + { + // Arrange + var switchActivity = new Switch + { + Cases = new List(), // Empty collection + Default = null // No default + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Handle_Mixed_Case_Conditions_In_MatchFirst_Mode() + { + // Arrange + var switchActivity = new Switch + { + Mode = new Input(SwitchMode.MatchFirst), + Cases = new List + { + new("First", Expression.LiteralExpression(false), Substitute.For()), + new("Second", Expression.LiteralExpression(true), Substitute.For()), + new("Third", Expression.LiteralExpression(true), Substitute.For()) + } + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Handle_Mixed_Case_Conditions_In_MatchAny_Mode() + { + // Arrange + var switchActivity = new Switch + { + Mode = new Input(SwitchMode.MatchAny), + Cases = new List + { + new("First", Expression.LiteralExpression(true), Substitute.For()), + new("Second", Expression.LiteralExpression(false), Substitute.For()), + new("Third", Expression.LiteralExpression(true), Substitute.For()) + } + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Use_MatchFirst_As_Default_Mode() + { + // Arrange + var switchActivity = new Switch + { + // No Mode explicitly set - should default to MatchFirst + Cases = new List + { + new("First True", Expression.LiteralExpression(true), Substitute.For()), + new("Second True", Expression.LiteralExpression(true), Substitute.For()) + } + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Handle_Null_Case_Condition_Gracefully() + { + // Arrange + var switchActivity = new Switch + { + Cases = new List + { + new("Null condition", Expression.LiteralExpression((bool?)null), Substitute.For()) + }, + Default = Substitute.For() + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Handle_Valid_Case_Conditions() + { + // Arrange + var switchActivity = new Switch + { + Cases = new List + { + new("Valid case", Expression.LiteralExpression(true), Substitute.For()) + } + }; + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Handle_SwitchCase_Collection_Properly() + { + // Arrange + var switchActivity = new Switch(); + + // Test that the Cases collection is initialized + Assert.NotNull(switchActivity.Cases); + Assert.Empty(switchActivity.Cases); + + // Test that we can add cases + switchActivity.Cases.Add(new SwitchCase("Test", Expression.LiteralExpression(true), Substitute.For())); + Assert.Single(switchActivity.Cases); + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } + + [Fact] + public async Task Should_Handle_Default_Mode_Setting() + { + // Arrange + var switchActivity = new Switch(); + + // Test that Mode has a default value + Assert.NotNull(switchActivity.Mode); + + // Test setting different modes + switchActivity.Mode = new Input(SwitchMode.MatchAny); + + // Act & Assert - Should not throw any exceptions + var exception = await Record.ExceptionAsync(async () => + await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + + Assert.Null(exception); + } +} From 59f3ef98bf00f39a0ac48cb4103780e1329fe899 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 20 Oct 2025 21:30:05 +0200 Subject: [PATCH 02/10] Remove `SendHttpRequestTests` and cleanup unused imports in `ActivityTestFixture`. --- .../ActivityTestFixture.cs | 4 - .../HTTP/SendHttpRequestTests.cs | 384 ------------------ 2 files changed, 388 deletions(-) delete mode 100644 test/unit/Elsa.Activities.UnitTests/HTTP/SendHttpRequestTests.cs diff --git a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs index ab1b7dbb1..8812a63b2 100644 --- a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs +++ b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs @@ -2,10 +2,7 @@ using Elsa.Common; using Elsa.Expressions.Contracts; using Elsa.Expressions.Services; using Elsa.Extensions; -using Elsa.Http.ContentWriters; -using Elsa.Http.Parsers; using Elsa.Mediator.Contracts; -using Elsa.Resilience; using Elsa.Workflows; using Elsa.Workflows.Activities; using Elsa.Workflows.CommitStates; @@ -16,7 +13,6 @@ using Elsa.Workflows.PortResolvers; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using NSubstitute; -using Elsa.Http; namespace Elsa.Testing.Shared; diff --git a/test/unit/Elsa.Activities.UnitTests/HTTP/SendHttpRequestTests.cs b/test/unit/Elsa.Activities.UnitTests/HTTP/SendHttpRequestTests.cs deleted file mode 100644 index 31a2df9bd..000000000 --- a/test/unit/Elsa.Activities.UnitTests/HTTP/SendHttpRequestTests.cs +++ /dev/null @@ -1,384 +0,0 @@ -using System.Net; -using System.Text; -using Elsa.Activities.UnitTests.Helpers; -using Elsa.Extensions; -using Elsa.Http; -using Elsa.Workflows; -using Elsa.Workflows.Attributes; -using Microsoft.Extensions.DependencyInjection; -using NSubstitute; - -namespace Elsa.Activities.UnitTests.HTTP; - -public class SendHttpRequestTests -{ - [Fact] - public async Task Should_Send_GET_Request_And_Handle_Success_Response() - { - // Arrange - var expectedUrl = new Uri("https://api.example.com/data"); - var mockHttpClientFactory = Substitute.For(); - - var responseContent = new StringContent("{\"result\": \"success\"}", Encoding.UTF8, "application/json"); - var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = responseContent - }; - - HttpRequestMessage? capturedRequest = null; - var testHandler = new TestHttpMessageHandler((request, _) => - { - capturedRequest = request; - return Task.FromResult(httpResponse); - }); - - var httpClient = new HttpClient(testHandler); - mockHttpClientFactory.CreateClient(Arg.Any()).Returns(httpClient); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(expectedUrl), - Method = new Input("GET"), - // Note: Setting ExpectedStatusCodes can cause workflow scheduling issues in test environments, - // because the activity may attempt to schedule additional branches for each expected status code, - // which can interfere with the test's control flow and assertions. To avoid this, we leave - // ExpectedStatusCodes empty in this test. - ExpectedStatusCodes = new List() - }; - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(sendHttpRequest, services => - { - services.AddSingleton(mockHttpClientFactory); - services.AddSingleton(ActivityTestHelper.CreateMockResilientActivityInvoker()); - ActivityTestHelper.AddHttpServices(services); - services.AddLogging(); - }); - - // Assert - Assert.NotNull(capturedRequest); - Assert.Equal(HttpMethod.Get, capturedRequest.Method); - Assert.Equal(expectedUrl, capturedRequest.RequestUri); - - var statusCodeOutput = context.GetExecutionOutput(_ => sendHttpRequest.StatusCode); - Assert.Equal(200, statusCodeOutput); - } - - [Fact] - public async Task Should_Send_POST_Request_With_JSON_Content() - { - // Arrange - var expectedUrl = new Uri("https://api.example.com/create"); - var requestContent = new { name = "test", value = 42 }; - var contentType = "application/json"; - var mockHttpClientFactory = Substitute.For(); - - var httpResponse = new HttpResponseMessage(HttpStatusCode.Created); - - HttpRequestMessage? capturedRequest = null; - var testHandler = new TestHttpMessageHandler((request, _) => - { - capturedRequest = request; - return Task.FromResult(httpResponse); - }); - - var httpClient = new HttpClient(testHandler); - mockHttpClientFactory.CreateClient(Arg.Any()).Returns(httpClient); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(expectedUrl), - Method = new Input("POST"), - Content = new Input(requestContent), - ContentType = new Input(contentType), - ExpectedStatusCodes = new List() - }; - - // Act - await ActivityTestHelper.ExecuteActivityAsync(sendHttpRequest, services => - { - services.AddSingleton(mockHttpClientFactory); - services.AddSingleton(ActivityTestHelper.CreateMockResilientActivityInvoker()); - ActivityTestHelper.AddHttpServices(services); - services.AddLogging(); - }); - - // Assert - Assert.NotNull(capturedRequest); - Assert.Equal(HttpMethod.Post, capturedRequest.Method); - Assert.Equal(expectedUrl, capturedRequest.RequestUri); - Assert.NotNull(capturedRequest.Content); - } - - [Fact] - public async Task Should_Add_Authorization_Header() - { - // Arrange - var expectedUrl = new Uri("https://api.example.com/secure"); - var authorizationHeader = "Bearer token123"; - var mockHttpClientFactory = Substitute.For(); - - var httpResponse = new HttpResponseMessage(HttpStatusCode.OK); - - HttpRequestMessage? capturedRequest = null; - var testHandler = new TestHttpMessageHandler((request, _) => - { - capturedRequest = request; - return Task.FromResult(httpResponse); - }); - - var httpClient = new HttpClient(testHandler); - mockHttpClientFactory.CreateClient(Arg.Any()).Returns(httpClient); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(expectedUrl), - Method = new Input("GET"), - Authorization = new Input(authorizationHeader), - ExpectedStatusCodes = new List() - }; - - // Act - await ActivityTestHelper.ExecuteActivityAsync(sendHttpRequest, services => - { - services.AddSingleton(mockHttpClientFactory); - services.AddSingleton(ActivityTestHelper.CreateMockResilientActivityInvoker()); - ActivityTestHelper.AddHttpServices(services); - services.AddLogging(); - }); - - // Assert - Assert.NotNull(capturedRequest); - Assert.NotNull(capturedRequest.Headers.Authorization); - Assert.Equal(authorizationHeader, capturedRequest.Headers.Authorization.ToString()); - } - - [Fact] - public Task Should_Execute_Matching_Status_Code_Activity() - { - // Arrange - var mockHttpClientFactory = Substitute.For(); - var mockHttpClient = Substitute.For(); - var mockActivity404 = Substitute.For(); - var mockActivity200 = Substitute.For(); - - var httpResponse = new HttpResponseMessage(HttpStatusCode.NotFound); - mockHttpClientFactory.CreateClient(Arg.Any()).Returns(mockHttpClient); - mockHttpClient.SendAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(httpResponse)); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(new Uri("https://api.example.com/notfound")), - Method = new Input("GET"), - // Test the configuration without actually scheduling activities - ExpectedStatusCodes = new List - { - new(200, mockActivity200), - new(404, mockActivity404) - } - }; - - // Act & Assert - This will fail due to scheduling, so we'll just test the configuration - var matchingCase = sendHttpRequest.ExpectedStatusCodes.FirstOrDefault(x => x.StatusCode == 404); - Assert.NotNull(matchingCase); - Assert.Equal(mockActivity404, matchingCase.Activity); - - // Test that we can identify the correct status code without execution - Assert.Equal(2, sendHttpRequest.ExpectedStatusCodes.Count); - return Task.CompletedTask; - } - - [Fact] - public Task Should_Execute_UnmatchedStatusCode_Activity_When_No_Match() - { - // Arrange - var mockUnmatchedActivity = Substitute.For(); - var mockActivity200 = Substitute.For(); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(new Uri("https://api.example.com/error")), - Method = new Input("GET"), - ExpectedStatusCodes = new List - { - new(200, mockActivity200) - }, - UnmatchedStatusCode = mockUnmatchedActivity - }; - - // Act & Assert - Test configuration without execution - var matchingCase = sendHttpRequest.ExpectedStatusCodes.FirstOrDefault(x => x.StatusCode == 500); - Assert.Null(matchingCase); - - // Verify UnmatchedStatusCode activity is set - Assert.Equal(mockUnmatchedActivity, sendHttpRequest.UnmatchedStatusCode); - return Task.CompletedTask; - } - - [Fact] - public Task Should_Handle_HttpRequestException_And_Execute_FailedToConnect_Activity() - { - // Arrange - var mockFailedToConnectActivity = Substitute.For(); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(new Uri("https://unreachable.example.com")), - Method = new Input("GET"), - FailedToConnect = mockFailedToConnectActivity - }; - - // Act & Assert - Test configuration without execution - Assert.Equal(mockFailedToConnectActivity, sendHttpRequest.FailedToConnect); - return Task.CompletedTask; - } - - [Fact] - public Task Should_Handle_TaskCanceledException_And_Execute_Timeout_Activity() - { - // Arrange - var mockTimeoutActivity = Substitute.For(); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(new Uri("https://slow.example.com")), - Method = new Input("GET"), - Timeout = mockTimeoutActivity - }; - - // Act & Assert - Test configuration without execution - Assert.Equal(mockTimeoutActivity, sendHttpRequest.Timeout); - return Task.CompletedTask; - } - - [Fact] - public async Task Should_Set_Response_Headers_Output() - { - // Arrange - var mockHttpClientFactory = Substitute.For(); - - var httpResponse = new HttpResponseMessage(HttpStatusCode.OK); - httpResponse.Headers.Add("Custom-Header", "CustomValue"); - httpResponse.Headers.Add("X-Rate-Limit", "100"); - - var testHandler = new TestHttpMessageHandler((_, _) => - Task.FromResult(httpResponse)); - - var httpClient = new HttpClient(testHandler); - mockHttpClientFactory.CreateClient(Arg.Any()).Returns(httpClient); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(new Uri("https://api.example.com/headers")), - Method = new Input("GET"), - ExpectedStatusCodes = new List() - }; - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(sendHttpRequest, services => - { - services.AddSingleton(mockHttpClientFactory); - services.AddSingleton(ActivityTestHelper.CreateMockResilientActivityInvoker()); - ActivityTestHelper.AddHttpServices(services); - services.AddLogging(); - }); - - // Assert - var responseHeadersObj = context.GetExecutionOutput(_ => sendHttpRequest.ResponseHeaders); - var responseHeaders = responseHeadersObj as HttpHeaders; - Assert.NotNull(responseHeaders); - Assert.True(responseHeaders.ContainsKey("Custom-Header")); - Assert.True(responseHeaders.ContainsKey("X-Rate-Limit")); - } - - [Fact] - public async Task Should_Parse_JSON_Response_Content() - { - // Arrange - var mockHttpClientFactory = Substitute.For(); - - var jsonContent = "{\"id\": 123, \"name\": \"test\"}"; - var responseContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - var httpResponse = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = responseContent - }; - - var testHandler = new TestHttpMessageHandler((_, _) => - Task.FromResult(httpResponse)); - - var httpClient = new HttpClient(testHandler); - mockHttpClientFactory.CreateClient(Arg.Any()).Returns(httpClient); - - var sendHttpRequest = new SendHttpRequest - { - Url = new Input(new Uri("https://api.example.com/json")), - Method = new Input("GET"), - ExpectedStatusCodes = new List() - }; - - // Act - var context = await ActivityTestHelper.ExecuteActivityAsync(sendHttpRequest, services => - { - services.AddSingleton(mockHttpClientFactory); - services.AddSingleton(ActivityTestHelper.CreateMockResilientActivityInvoker()); - ActivityTestHelper.AddHttpServices(services); - services.AddLogging(); - }); - - // Assert - var parsedContent = context.GetExecutionOutput(x => sendHttpRequest.ParsedContent); - var statusCode = context.GetExecutionOutput(x => sendHttpRequest.StatusCode); - var httpResponseResult = (HttpResponseMessage)context.GetExecutionOutput(x => sendHttpRequest.Result)!; - - Assert.NotNull(parsedContent); - Assert.Equal(200, statusCode); - - // Verify the response was received and stored - Assert.NotNull(httpResponseResult); - Assert.Equal(HttpStatusCode.OK, httpResponseResult.StatusCode); - } - - [Fact] - public void Should_Have_Correct_Activity_Attributes() - { - // Arrange & Act - var activityType = typeof(SendHttpRequest); - var activityAttribute = activityType.GetCustomAttributes(typeof(ActivityAttribute), false) - .Cast().FirstOrDefault(); - - // Assert - Assert.NotNull(activityAttribute); - Assert.Equal("Elsa", activityAttribute.Namespace); - Assert.Equal("HTTP", activityAttribute.Category); - Assert.Equal("Send an HTTP request.", activityAttribute.Description); - Assert.Equal("HTTP Request", activityAttribute.DisplayName); - Assert.Equal(ActivityKind.Task, activityAttribute.Kind); - } - - [Fact] - public void Should_Inherit_From_SendHttpRequestBase() - { - // Arrange & Act - var sendHttpRequest = new SendHttpRequest(); - - // Assert - Assert.IsAssignableFrom(sendHttpRequest); - } - - [Fact] - public void Should_Create_HttpStatusCodeCase_With_Status_And_Activity() - { - // Arrange - const int statusCode = 200; - var mockActivity = Substitute.For(); - - // Act - var httpStatusCodeCase = new HttpStatusCodeCase(statusCode, mockActivity); - - // Assert - Assert.Equal(statusCode, httpStatusCodeCase.StatusCode); - Assert.Equal(mockActivity, httpStatusCodeCase.Activity); - } -} From ccc2092c8f01f38e15082530aa05aefb3111e988 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 20 Oct 2025 21:34:14 +0200 Subject: [PATCH 03/10] Refactor `SwitchTests`: simplify syntax, replace `ActivityTestHelper` usage with direct `ExecuteAsync` method, and clean up unused imports. --- .../Branching/SwitchTests.cs | 79 ++++++++----------- 1 file changed, 34 insertions(+), 45 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs index 6d728866d..1709efd33 100644 --- a/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs @@ -1,5 +1,5 @@ -using Elsa.Activities.UnitTests.Helpers; using Elsa.Expressions.Models; +using Elsa.Testing.Shared; using Elsa.Workflows; using NSubstitute; @@ -14,8 +14,7 @@ public class SwitchTests var switchActivity = new Switch(); // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -35,8 +34,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -47,7 +45,7 @@ public class SwitchTests // Arrange var switchActivity = new Switch { - Mode = new Input(SwitchMode.MatchFirst), + Mode = new(SwitchMode.MatchFirst), Cases = new List { new("Case 1", Expression.LiteralExpression(false), Substitute.For()), @@ -57,8 +55,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -69,7 +66,7 @@ public class SwitchTests // Arrange var switchActivity = new Switch { - Mode = new Input(SwitchMode.MatchFirst), + Mode = new(SwitchMode.MatchFirst), Cases = new List { new("False Case", Expression.LiteralExpression(false), Substitute.For()), @@ -78,8 +75,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -90,7 +86,7 @@ public class SwitchTests // Arrange var switchActivity = new Switch { - Mode = new Input(SwitchMode.MatchFirst), + Mode = new(SwitchMode.MatchFirst), Cases = new List { new("First True", Expression.LiteralExpression(true), Substitute.For()), @@ -99,8 +95,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -111,7 +106,7 @@ public class SwitchTests // Arrange var switchActivity = new Switch { - Mode = new Input(SwitchMode.MatchAny), + Mode = new(SwitchMode.MatchAny), Cases = new List { new("First True", Expression.LiteralExpression(true), Substitute.For()), @@ -121,8 +116,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -133,7 +127,7 @@ public class SwitchTests // Arrange var switchActivity = new Switch { - Mode = new Input(SwitchMode.MatchAny), + Mode = new(SwitchMode.MatchAny), Cases = new List { new("Case 1", Expression.LiteralExpression(false), Substitute.For()), @@ -143,8 +137,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -163,8 +156,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -180,8 +172,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -197,8 +188,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -209,7 +199,7 @@ public class SwitchTests // Arrange var switchActivity = new Switch { - Mode = new Input(SwitchMode.MatchFirst), + Mode = new(SwitchMode.MatchFirst), Cases = new List { new("First", Expression.LiteralExpression(false), Substitute.For()), @@ -219,8 +209,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -231,7 +220,7 @@ public class SwitchTests // Arrange var switchActivity = new Switch { - Mode = new Input(SwitchMode.MatchAny), + Mode = new(SwitchMode.MatchAny), Cases = new List { new("First", Expression.LiteralExpression(true), Substitute.For()), @@ -241,8 +230,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -262,8 +250,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -276,14 +263,13 @@ public class SwitchTests { Cases = new List { - new("Null condition", Expression.LiteralExpression((bool?)null), Substitute.For()) + new("Null condition", Expression.LiteralExpression(null), Substitute.For()) }, Default = Substitute.For() }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -301,8 +287,7 @@ public class SwitchTests }; // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -318,12 +303,11 @@ public class SwitchTests Assert.Empty(switchActivity.Cases); // Test that we can add cases - switchActivity.Cases.Add(new SwitchCase("Test", Expression.LiteralExpression(true), Substitute.For())); + switchActivity.Cases.Add(new("Test", Expression.LiteralExpression(true), Substitute.For())); Assert.Single(switchActivity.Cases); // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } @@ -338,12 +322,17 @@ public class SwitchTests Assert.NotNull(switchActivity.Mode); // Test setting different modes - switchActivity.Mode = new Input(SwitchMode.MatchAny); + switchActivity.Mode = new(SwitchMode.MatchAny); // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(async () => - await ActivityTestHelper.ExecuteActivityAsync(switchActivity)); + var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); Assert.Null(exception); } + + private static Task ExecuteAsync(IActivity activity) + { + return new ActivityTestFixture(activity).ExecuteAsync(); + } + } From ba56fa9f9719733e42af9ccfe4cc05f460a221c7 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 21 Oct 2025 11:08:35 +0200 Subject: [PATCH 04/10] Improving cleanliness and usability of unit tests --- .../Branching/SwitchTests.cs | 398 ++++++++---------- 1 file changed, 174 insertions(+), 224 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs index 1709efd33..36250df06 100644 --- a/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs @@ -7,332 +7,282 @@ namespace Elsa.Activities.UnitTests.Branching; public class SwitchTests { - [Fact] - public async Task Should_Execute_Activity_Without_Errors() + [Theory] + [InlineData(SwitchMode.MatchFirst)] + [InlineData(SwitchMode.MatchAny)] + public async Task Should_Schedule_Default_Activity_When_No_Cases_Match(SwitchMode mode) { // Arrange - var switchActivity = new Switch(); + var defaultActivity = Substitute.For(); + var switchActivity = new Switch + { + Mode = new(mode), + Cases = new List + { + new("False Case", Expression.LiteralExpression(false), Substitute.For()) + }, + Default = defaultActivity + }; - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); + // Act + var context = await ExecuteAsync(switchActivity); + + // Assert + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Single(scheduledActivities); + Assert.Equal(defaultActivity, scheduledActivities.First().Activity); } [Fact] - public async Task Should_Execute_With_Default_Branch_When_No_Case_Matches() + public async Task Should_Schedule_No_Activities_When_No_Cases_Match_And_No_Default() { // Arrange - var mockActivity = Substitute.For(); var switchActivity = new Switch { Cases = new List { - new("Case 1", Expression.LiteralExpression(false), Substitute.For()) + new("False Case", Expression.LiteralExpression(false), Substitute.For()) }, - Default = mockActivity + Default = null }; - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); + // Act + var context = await ExecuteAsync(switchActivity); + + // Assert + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Empty(scheduledActivities); } [Fact] - public async Task Should_Execute_With_All_Cases_False_In_MatchFirst_Mode() - { - // Arrange - var switchActivity = new Switch - { - Mode = new(SwitchMode.MatchFirst), - Cases = new List - { - new("Case 1", Expression.LiteralExpression(false), Substitute.For()), - new("Case 2", Expression.LiteralExpression(false), Substitute.For()) - }, - Default = Substitute.For() - }; - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); - } - - [Fact] - public async Task Should_Execute_With_Single_True_Case_In_MatchFirst_Mode() + public async Task Should_Schedule_First_Matching_Case_In_MatchFirst_Mode() { // Arrange + var firstTrueActivity = Substitute.For(); + var secondTrueActivity = Substitute.For(); var switchActivity = new Switch { Mode = new(SwitchMode.MatchFirst), Cases = new List { new("False Case", Expression.LiteralExpression(false), Substitute.For()), - new("True Case", Expression.LiteralExpression(true), Substitute.For()) + new("First True", Expression.LiteralExpression(true), firstTrueActivity), + new("Second True", Expression.LiteralExpression(true), secondTrueActivity) } }; - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); + // Act + var context = await ExecuteAsync(switchActivity); + + // Assert + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Single(scheduledActivities); + Assert.Equal(firstTrueActivity, scheduledActivities.First().Activity); } [Fact] - public async Task Should_Execute_With_Multiple_True_Cases_In_MatchFirst_Mode() - { - // Arrange - var switchActivity = new Switch - { - Mode = new(SwitchMode.MatchFirst), - Cases = new List - { - new("First True", Expression.LiteralExpression(true), Substitute.For()), - new("Second True", Expression.LiteralExpression(true), Substitute.For()) - } - }; - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); - } - - [Fact] - public async Task Should_Execute_With_Multiple_True_Cases_In_MatchAny_Mode() + public async Task Should_Schedule_All_Matching_Cases_In_MatchAny_Mode() { // Arrange + var firstTrueActivity = Substitute.For(); + var secondTrueActivity = Substitute.For(); var switchActivity = new Switch { Mode = new(SwitchMode.MatchAny), Cases = new List { - new("First True", Expression.LiteralExpression(true), Substitute.For()), + new("First True", Expression.LiteralExpression(true), firstTrueActivity), new("False", Expression.LiteralExpression(false), Substitute.For()), - new("Second True", Expression.LiteralExpression(true), Substitute.For()) + new("Second True", Expression.LiteralExpression(true), secondTrueActivity) } }; - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); - } + // Act + var context = await ExecuteAsync(switchActivity); - [Fact] - public async Task Should_Execute_With_All_Cases_False_In_MatchAny_Mode() - { - // Arrange - var switchActivity = new Switch - { - Mode = new(SwitchMode.MatchAny), - Cases = new List - { - new("Case 1", Expression.LiteralExpression(false), Substitute.For()), - new("Case 2", Expression.LiteralExpression(false), Substitute.For()) - }, - Default = Substitute.For() - }; - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); - } - - [Fact] - public async Task Should_Execute_When_Default_Branch_Is_Absent_And_No_Matches() - { - // Arrange - var switchActivity = new Switch - { - Cases = new List - { - new("Case 1", Expression.LiteralExpression(false), Substitute.For()) - }, - Default = null // No default branch - }; - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); - } - - [Fact] - public async Task Should_Handle_Empty_Case_Collection() - { - // Arrange - var switchActivity = new Switch - { - Cases = new List(), // Empty collection - Default = Substitute.For() - }; - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); - } - - [Fact] - public async Task Should_Handle_Empty_Case_Collection_Without_Default() - { - // Arrange - var switchActivity = new Switch - { - Cases = new List(), // Empty collection - Default = null // No default - }; - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); - } - - [Fact] - public async Task Should_Handle_Mixed_Case_Conditions_In_MatchFirst_Mode() - { - // Arrange - var switchActivity = new Switch - { - Mode = new(SwitchMode.MatchFirst), - Cases = new List - { - new("First", Expression.LiteralExpression(false), Substitute.For()), - new("Second", Expression.LiteralExpression(true), Substitute.For()), - new("Third", Expression.LiteralExpression(true), Substitute.For()) - } - }; - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); - } - - [Fact] - public async Task Should_Handle_Mixed_Case_Conditions_In_MatchAny_Mode() - { - // Arrange - var switchActivity = new Switch - { - Mode = new(SwitchMode.MatchAny), - Cases = new List - { - new("First", Expression.LiteralExpression(true), Substitute.For()), - new("Second", Expression.LiteralExpression(false), Substitute.For()), - new("Third", Expression.LiteralExpression(true), Substitute.For()) - } - }; - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); + // Assert + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Equal(2, scheduledActivities.Count); + 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(); + var secondTrueActivity = Substitute.For(); var switchActivity = new Switch { // No Mode explicitly set - should default to MatchFirst Cases = new List { - new("First True", Expression.LiteralExpression(true), Substitute.For()), - new("Second True", Expression.LiteralExpression(true), Substitute.For()) + new("First True", Expression.LiteralExpression(true), firstTrueActivity), + new("Second True", Expression.LiteralExpression(true), secondTrueActivity) } }; - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); + // Act + var context = await ExecuteAsync(switchActivity); + + // Assert - Should only schedule the first matching case, proving MatchFirst is the default + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Single(scheduledActivities); + Assert.Equal(firstTrueActivity, scheduledActivities.First().Activity); } - [Fact] - public async Task Should_Handle_Null_Case_Condition_Gracefully() + [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(); var switchActivity = new Switch { + Mode = new(mode), Cases = new List { new("Null condition", Expression.LiteralExpression(null), Substitute.For()) }, - Default = Substitute.For() + Default = defaultActivity }; - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); + // Act + var context = await ExecuteAsync(switchActivity); + + // Assert - Null conditions should not match, so default should be scheduled + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Single(scheduledActivities); + Assert.Equal(defaultActivity, scheduledActivities.First().Activity); } - [Fact] - public async Task Should_Handle_Valid_Case_Conditions() + [Theory] + [InlineData(SwitchMode.MatchFirst)] + [InlineData(SwitchMode.MatchAny)] + public async Task Should_Schedule_No_Activities_When_Empty_Cases_And_No_Default(SwitchMode mode) { // Arrange var switchActivity = new Switch { - Cases = new List - { - new("Valid case", Expression.LiteralExpression(true), Substitute.For()) - } + Mode = new(mode), + Cases = new List(), // Empty collection + Default = null }; - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); + // Act + var context = await ExecuteAsync(switchActivity); + + // Assert + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Empty(scheduledActivities); + } + + [Theory] + [InlineData(SwitchMode.MatchFirst)] + [InlineData(SwitchMode.MatchAny)] + public async Task Should_Schedule_Default_When_Empty_Cases_And_Default_Present(SwitchMode mode) + { + // Arrange + var defaultActivity = Substitute.For(); + var switchActivity = new Switch + { + Mode = new(mode), + Cases = new List(), // Empty collection + 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); } [Fact] - public async Task Should_Handle_SwitchCase_Collection_Properly() + public void Should_Initialize_Cases_Collection_By_Default() { - // Arrange + // Arrange & Act var switchActivity = new Switch(); - // Test that the Cases collection is initialized + // Assert Assert.NotNull(switchActivity.Cases); Assert.Empty(switchActivity.Cases); // Test that we can add cases switchActivity.Cases.Add(new("Test", Expression.LiteralExpression(true), Substitute.For())); Assert.Single(switchActivity.Cases); - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); } [Fact] - public async Task Should_Handle_Default_Mode_Setting() + public void Should_Initialize_Mode_To_MatchFirst_By_Default() { // Arrange var switchActivity = new Switch(); - // Test that Mode has a default value + // Assert Assert.NotNull(switchActivity.Mode); - - // Test setting different modes - switchActivity.Mode = new(SwitchMode.MatchAny); - - // Act & Assert - Should not throw any exceptions - var exception = await Record.ExceptionAsync(() => ExecuteAsync(switchActivity)); - - Assert.Null(exception); + // The actual default value verification is handled by the mode-specific behavior tests + } + + [Fact] + public async Task Should_Handle_Mixed_True_And_False_Cases_In_MatchFirst_Mode() + { + // Arrange + var firstTrueActivity = Substitute.For(); + var secondTrueActivity = Substitute.For(); + var switchActivity = new Switch + { + Mode = new(SwitchMode.MatchFirst), + Cases = new List + { + new("False", Expression.LiteralExpression(false), Substitute.For()), + new("First True", Expression.LiteralExpression(true), firstTrueActivity), + new("Second True", Expression.LiteralExpression(true), secondTrueActivity) + } + }; + + // Act + var context = await ExecuteAsync(switchActivity); + + // Assert - Should only schedule the first true case + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Single(scheduledActivities); + Assert.Equal(firstTrueActivity, scheduledActivities.First().Activity); + } + + [Fact] + public async Task Should_Handle_Mixed_True_And_False_Cases_In_MatchAny_Mode() + { + // Arrange + var firstTrueActivity = Substitute.For(); + var secondTrueActivity = Substitute.For(); + var switchActivity = new Switch + { + Mode = new(SwitchMode.MatchAny), + Cases = new List + { + new("First True", Expression.LiteralExpression(true), firstTrueActivity), + new("False", Expression.LiteralExpression(false), Substitute.For()), + new("Second True", Expression.LiteralExpression(true), secondTrueActivity) + } + }; + + // Act + var context = await ExecuteAsync(switchActivity); + + // Assert - Should schedule all true cases + var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Equal(2, scheduledActivities.Count); + Assert.Contains(scheduledActivities, s => s.Activity == firstTrueActivity); + Assert.Contains(scheduledActivities, s => s.Activity == secondTrueActivity); } private static Task ExecuteAsync(IActivity activity) { return new ActivityTestFixture(activity).ExecuteAsync(); } - } From 952a9465370cfb8e78380b5ec232ed49d86cab5f Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 21 Oct 2025 12:01:24 +0200 Subject: [PATCH 05/10] Grouping tests into theories --- .../ActivityTestFixture.cs | 2 +- .../Branching/SwitchTests.cs | 192 +++++------------- 2 files changed, 50 insertions(+), 144 deletions(-) diff --git a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs index 8812a63b2..7a155fc78 100644 --- a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs +++ b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs @@ -58,7 +58,7 @@ public class ActivityTestFixture configure(Services); return this; } - + /// /// Configures the activity execution context before execution. /// Multiple calls to this method will chain the configuration actions together. diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs index 36250df06..c0758bbe4 100644 --- a/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Branching/SwitchTests.cs @@ -8,12 +8,14 @@ namespace Elsa.Activities.UnitTests.Branching; public class SwitchTests { [Theory] - [InlineData(SwitchMode.MatchFirst)] - [InlineData(SwitchMode.MatchAny)] - public async Task Should_Schedule_Default_Activity_When_No_Cases_Match(SwitchMode mode) + [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 = Substitute.For(); + var defaultActivity = hasDefault ? Substitute.For() : null; var switchActivity = new Switch { Mode = new(mode), @@ -29,70 +31,32 @@ public class SwitchTests // Assert var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); - Assert.Single(scheduledActivities); - Assert.Equal(defaultActivity, scheduledActivities.First().Activity); - } - - [Fact] - public async Task Should_Schedule_No_Activities_When_No_Cases_Match_And_No_Default() - { - // Arrange - var switchActivity = new Switch + if (hasDefault) { - Cases = new List - { - new("False Case", Expression.LiteralExpression(false), Substitute.For()) - }, - Default = null - }; - - // Act - var context = await ExecuteAsync(switchActivity); - - // Assert - var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); - Assert.Empty(scheduledActivities); + Assert.Single(scheduledActivities); + Assert.Equal(defaultActivity, scheduledActivities.First().Activity); + } + else + { + Assert.Empty(scheduledActivities); + } } - [Fact] - public async Task Should_Schedule_First_Matching_Case_In_MatchFirst_Mode() + [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(); var secondTrueActivity = Substitute.For(); var switchActivity = new Switch { - Mode = new(SwitchMode.MatchFirst), + Mode = new(mode), Cases = new List { - new("False Case", Expression.LiteralExpression(false), Substitute.For()), - 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); - } - - [Fact] - public async Task Should_Schedule_All_Matching_Cases_In_MatchAny_Mode() - { - // Arrange - var firstTrueActivity = Substitute.For(); - var secondTrueActivity = Substitute.For(); - var switchActivity = new Switch - { - Mode = new(SwitchMode.MatchAny), - Cases = new List - { - new("First True", Expression.LiteralExpression(true), firstTrueActivity), new("False", Expression.LiteralExpression(false), Substitute.For()), + new("First True", Expression.LiteralExpression(true), firstTrueActivity), new("Second True", Expression.LiteralExpression(true), secondTrueActivity) } }; @@ -102,9 +66,17 @@ public class SwitchTests // Assert var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); - Assert.Equal(2, scheduledActivities.Count); - Assert.Contains(scheduledActivities, s => s.Activity == firstTrueActivity); - Assert.Contains(scheduledActivities, s => s.Activity == secondTrueActivity); + 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] @@ -126,7 +98,7 @@ public class SwitchTests // Act var context = await ExecuteAsync(switchActivity); - // Assert - Should only schedule the first matching case, proving MatchFirst is the default + // Assert var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); Assert.Single(scheduledActivities); Assert.Equal(firstTrueActivity, scheduledActivities.First().Activity); @@ -152,44 +124,25 @@ public class SwitchTests // Act var context = await ExecuteAsync(switchActivity); - // Assert - Null conditions should not match, so default should be scheduled + // Assert var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); Assert.Single(scheduledActivities); Assert.Equal(defaultActivity, scheduledActivities.First().Activity); } [Theory] - [InlineData(SwitchMode.MatchFirst)] - [InlineData(SwitchMode.MatchAny)] - public async Task Should_Schedule_No_Activities_When_Empty_Cases_And_No_Default(SwitchMode mode) + [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() : null; var switchActivity = new Switch { Mode = new(mode), - Cases = new List(), // Empty collection - Default = null - }; - - // Act - var context = await ExecuteAsync(switchActivity); - - // Assert - var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); - Assert.Empty(scheduledActivities); - } - - [Theory] - [InlineData(SwitchMode.MatchFirst)] - [InlineData(SwitchMode.MatchAny)] - public async Task Should_Schedule_Default_When_Empty_Cases_And_Default_Present(SwitchMode mode) - { - // Arrange - var defaultActivity = Substitute.For(); - var switchActivity = new Switch - { - Mode = new(mode), - Cases = new List(), // Empty collection + Cases = new List(), Default = defaultActivity }; @@ -198,8 +151,15 @@ public class SwitchTests // Assert var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); - Assert.Single(scheduledActivities); - Assert.Equal(defaultActivity, scheduledActivities.First().Activity); + if (hasDefault) + { + Assert.Single(scheduledActivities); + Assert.Equal(defaultActivity, scheduledActivities.First().Activity); + } + else + { + Assert.Empty(scheduledActivities); + } } [Fact] @@ -212,7 +172,6 @@ public class SwitchTests Assert.NotNull(switchActivity.Cases); Assert.Empty(switchActivity.Cases); - // Test that we can add cases switchActivity.Cases.Add(new("Test", Expression.LiteralExpression(true), Substitute.For())); Assert.Single(switchActivity.Cases); } @@ -227,59 +186,6 @@ public class SwitchTests Assert.NotNull(switchActivity.Mode); // The actual default value verification is handled by the mode-specific behavior tests } - - [Fact] - public async Task Should_Handle_Mixed_True_And_False_Cases_In_MatchFirst_Mode() - { - // Arrange - var firstTrueActivity = Substitute.For(); - var secondTrueActivity = Substitute.For(); - var switchActivity = new Switch - { - Mode = new(SwitchMode.MatchFirst), - Cases = new List - { - new("False", Expression.LiteralExpression(false), Substitute.For()), - new("First True", Expression.LiteralExpression(true), firstTrueActivity), - new("Second True", Expression.LiteralExpression(true), secondTrueActivity) - } - }; - - // Act - var context = await ExecuteAsync(switchActivity); - - // Assert - Should only schedule the first true case - var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); - Assert.Single(scheduledActivities); - Assert.Equal(firstTrueActivity, scheduledActivities.First().Activity); - } - - [Fact] - public async Task Should_Handle_Mixed_True_And_False_Cases_In_MatchAny_Mode() - { - // Arrange - var firstTrueActivity = Substitute.For(); - var secondTrueActivity = Substitute.For(); - var switchActivity = new Switch - { - Mode = new(SwitchMode.MatchAny), - Cases = new List - { - new("First True", Expression.LiteralExpression(true), firstTrueActivity), - new("False", Expression.LiteralExpression(false), Substitute.For()), - new("Second True", Expression.LiteralExpression(true), secondTrueActivity) - } - }; - - // Act - var context = await ExecuteAsync(switchActivity); - - // Assert - Should schedule all true cases - var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); - Assert.Equal(2, scheduledActivities.Count); - Assert.Contains(scheduledActivities, s => s.Activity == firstTrueActivity); - Assert.Contains(scheduledActivities, s => s.Activity == secondTrueActivity); - } private static Task ExecuteAsync(IActivity activity) { From bf5520f395b4d13e97737a0fa46c9c3a3cb3fff6 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 21 Oct 2025 12:03:05 +0200 Subject: [PATCH 06/10] fixing formatting --- src/common/Elsa.Testing.Shared/ActivityTestFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs index 7a155fc78..8412d2189 100644 --- a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs +++ b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs @@ -167,4 +167,4 @@ public class ActivityTestFixture services.AddSingleton(); services.AddSingleton(); } -} +} \ No newline at end of file From f7b7ea4d730ad49893ae200c2b720ecc472fcf03 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Oct 2025 15:31:31 +0200 Subject: [PATCH 07/10] Add unit tests for `FlowFork` activity and enhance test utilities (#6995) * Add unit tests for `FlowFork` activity and enhance test utilities - Introduce `FlowForkTests` to validate activity behavior with various branch configurations. - Extend `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes. - Modify `ActivityTestFixture` to ensure activities transition to `Running` status before execution. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ActivityExecutionContextExtensions.cs | 12 ++++++ .../ActivityTestFixture.cs | 1 + .../Branching/FlowForkTests.cs | 40 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 test/unit/Elsa.Activities.UnitTests/Branching/FlowForkTests.cs diff --git a/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs b/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs index fc5f5bff3..6af31b080 100644 --- a/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs +++ b/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs @@ -17,4 +17,16 @@ public static class ActivityExecutionContextExtensions { return activityExecutionContext.WorkflowExecutionContext.Scheduler.Find(x => x.Activity == activity) != null; } + + public static IEnumerable GetOutcomes(this ActivityExecutionContext activityExecutionContext) + { + if (activityExecutionContext.JournalData.TryGetValue("Outcomes", out var outcomesObj) && outcomesObj is string[] outcomes) + return outcomes; + return []; + } + + public static bool HasOutcome(this ActivityExecutionContext activityExecutionContext, string outcome) + { + return activityExecutionContext.GetOutcomes().Contains(outcome); + } } \ No newline at end of file diff --git a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs index 8412d2189..0b077d1c9 100644 --- a/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs +++ b/src/common/Elsa.Testing.Shared/ActivityTestFixture.cs @@ -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; diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/FlowForkTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/FlowForkTests.cs new file mode 100644 index 000000000..8d1f74580 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Branching/FlowForkTests.cs @@ -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 BranchTestCases() + { + yield return [Array.Empty(), new[] { "Done" }]; + yield return [new[] { "SingleBranch" }, new[] { "SingleBranch" }]; + yield return [new[] { "Branch1", "Branch2", "Branch3" }, new[] { "Branch1", "Branch2", "Branch3" }]; + } + + private static Task ExecuteAsync(IActivity activity) + { + return new ActivityTestFixture(activity).ExecuteAsync(); + } +} From d0a4520e7d04977bc8bf55fdad54bf68a42b5430 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Oct 2025 15:36:12 +0200 Subject: [PATCH 08/10] Adds unit tests for FlowSwitch activity (#6996) * Add unit tests for `FlowFork` activity and enhance test utilities - Introduce `FlowForkTests` to validate activity behavior with various branch configurations. - Extend `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes. - Modify `ActivityTestFixture` to ensure activities transition to `Running` status before execution. * Add unit tests for `FlowSwitch` activity and extend test utilities - Implement `FlowSwitchTests` to verify behavior based on switch cases, modes, and literal expressions. - Enhance `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes in execution context. * Update test guidelines to include examples for checking activity outcomes - Add unit test examples for validating multiple and default outcomes. - Extend documentation to describe new `ActivityExecutionContextExtensions` methods: `GetOutcomes` and `HasOutcome`. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify `GetOutcomes` method in `ActivityExecutionContextExtensions`. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- doc/qa/test-guidelines.md | 38 +++++++ .../ActivityExecutionContextExtensions.cs | 19 +++- .../Branching/FlowSwitchTests.cs | 99 +++++++++++++++++++ 3 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 test/unit/Elsa.Activities.UnitTests/Branching/FlowSwitchTests.cs diff --git a/doc/qa/test-guidelines.md b/doc/qa/test-guidelines.md index de0b29cc1..81feb0519 100644 --- a/doc/qa/test-guidelines.md +++ b/doc/qa/test-guidelines.md @@ -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.
Integration tests only | diff --git a/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs b/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs index 6af31b080..3f9d03d9d 100644 --- a/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs +++ b/src/common/Elsa.Testing.Shared/ActivityExecutionContextExtensions.cs @@ -17,14 +17,23 @@ public static class ActivityExecutionContextExtensions { return activityExecutionContext.WorkflowExecutionContext.Scheduler.Find(x => x.Activity == activity) != null; } - + + /// + /// Retrieves the collection of outcomes recorded in the execution context's journal data. + /// + /// The activity execution context from which to retrieve the outcomes. + /// A collection of outcome strings, or an empty collection if no outcomes are present. public static IEnumerable GetOutcomes(this ActivityExecutionContext activityExecutionContext) { - if (activityExecutionContext.JournalData.TryGetValue("Outcomes", out var outcomesObj) && outcomesObj is string[] outcomes) - return outcomes; - return []; + return activityExecutionContext.JournalData.TryGetValue("Outcomes", out var outcomes) && outcomes is string[] arr ? arr : []; } - + + /// + /// Determines whether the specified outcome exists in the current activity execution context. + /// + /// The activity execution context to check for the outcome. + /// The outcome to verify within the execution context. + /// True if the specified outcome exists; otherwise, false. public static bool HasOutcome(this ActivityExecutionContext activityExecutionContext, string outcome) { return activityExecutionContext.GetOutcomes().Contains(outcome); diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/FlowSwitchTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/FlowSwitchTests.cs new file mode 100644 index 000000000..bbaff3172 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Branching/FlowSwitchTests.cs @@ -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 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 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 + { + 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 DefaultOutcomeTestCases() + { + yield return [new List()]; + yield return + [ + new List + { + new("Case1", () => false), + new("Case2", () => false) + } + ]; + } + + public static IEnumerable SwitchModeTestCases() + { + var cases = new List + { + 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 ExecuteAsync(IActivity activity) + { + return new ActivityTestFixture(activity).ExecuteAsync(); + } +} From 84c04341f947b12f82c452af7e1a77c1291dab29 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Oct 2025 15:36:54 +0200 Subject: [PATCH 09/10] Add unit tests for `FlowSendHttpRequest` activity and refactor helper utilities (#6997) * Add unit tests for `FlowFork` activity and enhance test utilities - Introduce `FlowForkTests` to validate activity behavior with various branch configurations. - Extend `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes. - Modify `ActivityTestFixture` to ensure activities transition to `Running` status before execution. * Add unit tests for `FlowSwitch` activity and extend test utilities - Implement `FlowSwitchTests` to verify behavior based on switch cases, modes, and literal expressions. - Enhance `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes in execution context. * Update test guidelines to include examples for checking activity outcomes - Add unit test examples for validating multiple and default outcomes. - Extend documentation to describe new `ActivityExecutionContextExtensions` methods: `GetOutcomes` and `HasOutcome`. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify `GetOutcomes` method in `ActivityExecutionContextExtensions`. * Add unit tests for `FlowSendHttpRequest` activity and refactor helper utilities - Introduce `FlowSendHttpRequestTests` to validate activity behavior across various scenarios. - Add utility methods in `SendHttpRequestTestHelpers` for creating mock responses and exceptions. - Refactor `Helpers` namespace to `Http.Helpers` for better organization. - Update project file to reflect namespace adjustments. * Update test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Http/FlowSendHttpRequestTests.cs | 206 ++++++++++++++++++ .../ActivityTestFixtureHttpExtensions.cs | 2 +- .../Helpers/SendHttpRequestTestHelpers.cs | 43 ++++ .../Helpers/TestHttpMessageHandler.cs | 2 +- .../Http/SendHttpRequestTests.cs | 2 +- 5 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 test/unit/Elsa.Activities.UnitTests/Http/FlowSendHttpRequestTests.cs rename test/unit/Elsa.Activities.UnitTests/{ => Http}/Helpers/ActivityTestFixtureHttpExtensions.cs (99%) create mode 100644 test/unit/Elsa.Activities.UnitTests/Http/Helpers/SendHttpRequestTestHelpers.cs rename test/unit/Elsa.Activities.UnitTests/{ => Http}/Helpers/TestHttpMessageHandler.cs (93%) diff --git a/test/unit/Elsa.Activities.UnitTests/Http/FlowSendHttpRequestTests.cs b/test/unit/Elsa.Activities.UnitTests/Http/FlowSendHttpRequestTests.cs new file mode 100644 index 000000000..5dfa4a3f4 --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Http/FlowSendHttpRequestTests.cs @@ -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 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(), 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("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("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 + { + { "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; + 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(content) : null!, + ContentType = contentType != null ? new Input(contentType) : null!, + Authorization = authorization != null ? new Input(authorization) : null!, + ExpectedStatusCodes = expectedStatusCodes != null ? new Input>(expectedStatusCodes) : null! + }; + } + + private static Task ExecuteActivityAsync( + FlowSendHttpRequest flowSendHttpRequest, + Func> responseHandler) + { + return new ActivityTestFixture(flowSendHttpRequest).WithHttpServices(responseHandler).ExecuteAsync(); + } +} diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestFixtureHttpExtensions.cs b/test/unit/Elsa.Activities.UnitTests/Http/Helpers/ActivityTestFixtureHttpExtensions.cs similarity index 99% rename from test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestFixtureHttpExtensions.cs rename to test/unit/Elsa.Activities.UnitTests/Http/Helpers/ActivityTestFixtureHttpExtensions.cs index ea6b8de4f..0cc07b367 100644 --- a/test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestFixtureHttpExtensions.cs +++ b/test/unit/Elsa.Activities.UnitTests/Http/Helpers/ActivityTestFixtureHttpExtensions.cs @@ -9,7 +9,7 @@ using Elsa.Workflows; using Microsoft.Extensions.DependencyInjection; using NSubstitute; -namespace Elsa.Activities.UnitTests.Helpers; +namespace Elsa.Activities.UnitTests.Http.Helpers; /// /// Extension methods for configuring HTTP-related services in ActivityTestFixture. diff --git a/test/unit/Elsa.Activities.UnitTests/Http/Helpers/SendHttpRequestTestHelpers.cs b/test/unit/Elsa.Activities.UnitTests/Http/Helpers/SendHttpRequestTestHelpers.cs new file mode 100644 index 000000000..f654201ac --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Http/Helpers/SendHttpRequestTestHelpers.cs @@ -0,0 +1,43 @@ +using System.Net; + +namespace Elsa.Activities.UnitTests.Http.Helpers; + +/// +/// Shared helper methods for testing SendHttpRequest and FlowSendHttpRequest activities. +/// +public static class SendHttpRequestTestHelpers +{ + /// + /// Creates a response handler that returns a specific HTTP status code and optional content. + /// + public static Func> CreateResponseHandler( + HttpStatusCode statusCode, + string? content = null, + RequestCapture? requestCapture = null, + Dictionary? additionalHeaders = null) + { + return (request, _) => + { + if (requestCapture != null) + requestCapture.CapturedRequest = request; + return Task.FromResult(ActivityTestFixtureHttpExtensions.CreateHttpResponse(statusCode, content, additionalHeaders)); + }; + } + + /// + /// Creates an exception handler that throws a specific exception type with a message. + /// + public static Func> CreateExceptionHandler(string message) + where TException : Exception + { + return (_, _) => throw ((TException)Activator.CreateInstance(typeof(TException), message)!); + } + + /// + /// Captures HTTP request details during test execution. + /// + public sealed class RequestCapture + { + public HttpRequestMessage? CapturedRequest { get; set; } + } +} diff --git a/test/unit/Elsa.Activities.UnitTests/Helpers/TestHttpMessageHandler.cs b/test/unit/Elsa.Activities.UnitTests/Http/Helpers/TestHttpMessageHandler.cs similarity index 93% rename from test/unit/Elsa.Activities.UnitTests/Helpers/TestHttpMessageHandler.cs rename to test/unit/Elsa.Activities.UnitTests/Http/Helpers/TestHttpMessageHandler.cs index 0b9f1843d..2eb0fbfbb 100644 --- a/test/unit/Elsa.Activities.UnitTests/Helpers/TestHttpMessageHandler.cs +++ b/test/unit/Elsa.Activities.UnitTests/Http/Helpers/TestHttpMessageHandler.cs @@ -1,4 +1,4 @@ -namespace Elsa.Activities.UnitTests.Helpers; +namespace Elsa.Activities.UnitTests.Http.Helpers; /// /// Custom test HTTP message handler that allows full control over HTTP responses for testing. diff --git a/test/unit/Elsa.Activities.UnitTests/Http/SendHttpRequestTests.cs b/test/unit/Elsa.Activities.UnitTests/Http/SendHttpRequestTests.cs index 71a32a7bb..62c9e4f21 100644 --- a/test/unit/Elsa.Activities.UnitTests/Http/SendHttpRequestTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Http/SendHttpRequestTests.cs @@ -1,5 +1,5 @@ using System.Net; -using Elsa.Activities.UnitTests.Helpers; +using Elsa.Activities.UnitTests.Http.Helpers; using Elsa.Extensions; using Elsa.Http; using Elsa.Testing.Shared; From 66543a48b0d9b486fba1f5f1a0fed58635ae55dd Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 21 Oct 2025 15:37:10 +0200 Subject: [PATCH 10/10] Add integration tests for WorkflowDefinitionActivity scenarios (#6999) * Add unit tests for `FlowFork` activity and enhance test utilities - Introduce `FlowForkTests` to validate activity behavior with various branch configurations. - Extend `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes. - Modify `ActivityTestFixture` to ensure activities transition to `Running` status before execution. * Add unit tests for `FlowSwitch` activity and extend test utilities - Implement `FlowSwitchTests` to verify behavior based on switch cases, modes, and literal expressions. - Enhance `ActivityExecutionContextExtensions` with methods to retrieve and check outcomes in execution context. * Update test guidelines to include examples for checking activity outcomes - Add unit test examples for validating multiple and default outcomes. - Extend documentation to describe new `ActivityExecutionContextExtensions` methods: `GetOutcomes` and `HasOutcome`. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify `GetOutcomes` method in `ActivityExecutionContextExtensions`. * Add unit tests for `FlowSendHttpRequest` activity and refactor helper utilities - Introduce `FlowSendHttpRequestTests` to validate activity behavior across various scenarios. - Add utility methods in `SendHttpRequestTestHelpers` for creating mock responses and exceptions. - Refactor `Helpers` namespace to `Http.Helpers` for better organization. - Update project file to reflect namespace adjustments. * Update test/unit/Elsa.Activities.UnitTests/Elsa.Activities.UnitTests.csproj Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add integration tests for WorkflowDefinitionActivity scenarios - Introduce integration tests covering inputs, outputs, version fallback, and outcome handling. - Add JSON workflow definitions to validate parent-child workflow execution. - Update project files to include new test scenarios. - Enhance error handling in `RunWorkflowExtensions` for missing workflow definitions. - Adjust JavaScript expression handler to support PascalCase activity names. * Refactor WorkflowDefinitionActivity tests to use Theory with dynamic test cases - Consolidate multiple Fact-based tests into a single parameterized Theory. - Introduce `WorkflowDefinitionActivityTestCases` for reusable test scenarios. - Simplify assertions and improve test structure for better maintainability. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../RunWorkflowExtensions.cs | 6 +- ...ineWithVariablesAndInputOutputAccessors.cs | 2 +- .../Elsa.Workflows.IntegrationTests.csproj | 27 ++++ .../Scenarios/DependencyWorkflows/Tests.cs | 5 +- .../WorkflowDefinitionActivities/Tests.cs | 41 +++++ .../Workflows/child-published-version.json | 23 +++ .../Workflows/child-with-input.json | 79 ++++++++++ .../Workflows/child-with-outcomes.json | 72 +++++++++ .../Workflows/child-with-output.json | 84 ++++++++++ .../Workflows/parent-version-fallback.json | 23 +++ .../Workflows/parent-with-input.json | 74 +++++++++ .../Workflows/parent-with-io.json | 23 +++ .../Workflows/parent-with-outcomes.json | 147 ++++++++++++++++++ .../Workflows/parent-with-output.json | 107 +++++++++++++ 14 files changed, 709 insertions(+), 4 deletions(-) create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Tests.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-published-version.json create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-input.json create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-outcomes.json create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-output.json create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-version-fallback.json create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-input.json create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-io.json create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-outcomes.json create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-output.json diff --git a/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs b/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs index 25e8b3ce8..429ce6f99 100644 --- a/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs +++ b/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs @@ -35,11 +35,15 @@ public static class RunWorkflowExtensions { var workflowDefinitionService = services.GetRequiredService(); 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(); 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 }); diff --git a/src/modules/Elsa.Expressions.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs b/src/modules/Elsa.Expressions.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs index 6a443629d..c1f38f9ae 100644 --- a/src/modules/Elsa.Expressions.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs +++ b/src/modules/Elsa.Expressions.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs @@ -75,6 +75,6 @@ public class ConfigureEngineWithVariablesAndInputOutputAccessors(IOptions)(() => context.GetOutput(activityOutput.ActivityId, outputName))); + engine.SetValue($"get{outputName}From{activityOutput.ActivityName.Pascalize()}", (Func)(() => context.GetOutput(activityOutput.ActivityId, outputName))); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj b/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj index f3487fa1f..08c0420f9 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj +++ b/test/integration/Elsa.Workflows.IntegrationTests/Elsa.Workflows.IntegrationTests.csproj @@ -35,5 +35,32 @@ Always + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/DependencyWorkflows/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/DependencyWorkflows/Tests.cs index 45b4a0dca..f90bd7c9f 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/DependencyWorkflows/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/DependencyWorkflows/Tests.cs @@ -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. diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Tests.cs new file mode 100644 index 000000000..453054974 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Tests.cs @@ -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 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"]; + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-published-version.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-published-version.json new file mode 100644 index 000000000..23cf02e78 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-published-version.json @@ -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" + } + } + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-input.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-input.json new file mode 100644 index 000000000..3b85bc4bf --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-input.json @@ -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": [] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-outcomes.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-outcomes.json new file mode 100644 index 000000000..6a4aeeba8 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-outcomes.json @@ -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": [] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-output.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-output.json new file mode 100644 index 000000000..fab43afa4 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/child-with-output.json @@ -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": [] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-version-fallback.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-version-fallback.json new file mode 100644 index 000000000..b750cb617 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-version-fallback.json @@ -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" + } + ] + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-input.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-input.json new file mode 100644 index 000000000..2293ba19f --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-input.json @@ -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": [] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-io.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-io.json new file mode 100644 index 000000000..526e7396b --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-io.json @@ -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" + } + } + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-outcomes.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-outcomes.json new file mode 100644 index 000000000..887f6f04e --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-outcomes.json @@ -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": [] + } + ] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-output.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-output.json new file mode 100644 index 000000000..e1c7dd555 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowDefinitionActivities/Workflows/parent-with-output.json @@ -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": [] + } + ] + } +} \ No newline at end of file