From 0c6eea5edf587a4f13bbafd63bd7870fa3efc23b Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 21 Oct 2025 16:52:17 +0200 Subject: [PATCH 1/6] Unit tests for For activity --- .../Looping/ForTests.cs | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs diff --git a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs new file mode 100644 index 000000000..2dca772eb --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs @@ -0,0 +1,255 @@ +using Elsa.Testing.Shared; +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Behaviors; + +namespace Elsa.Activities.UnitTests.Looping; + +public class ForTests +{ + [Theory] + [InlineData(1, 3, 1)] // Ascending loop + [InlineData(5, 1, -1)] // Descending loop + [InlineData(10, 12, 1)] // UpdatesCurrentValueEachIteration + [InlineData(0, 4, 1)] // HandlesFloatingPointSteps equivalent + [InlineData(10, 15, 1)] // PreservesIterationCountAccuracy + [InlineData(-1, -5, -1)] // NegativeRangeAndNegativeStep + public async Task ExecutesValidLoopConfigurations_SchedulesChildActivity(int start, int end, int step) + { + // Arrange + var mockBody = new MockBodyActivity(); + var forActivity = new For(start, end, step) { Body = mockBody }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.Equal(start, currentValue); + + // Verify that child activity is scheduled for valid configurations + Assert.True(context.HasScheduledActivity(mockBody), "Expected child activity to be scheduled"); + } + + [Theory] + [InlineData(1, 3, true)] // Inclusive bounds + [InlineData(1, 3, false)] // Exclusive bounds + [InlineData(5, 5, true)] // Empty range inclusive + [InlineData(5, 5, false)] // Empty range exclusive + public async Task ExecutesBoundaryConditions(int start, int end, bool inclusive) + { + // Arrange + var mockBody = new MockBodyActivity(); + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input(1), + OuterBoundInclusive = new Input(inclusive), + Body = mockBody + }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + bool shouldSchedule = ShouldExecuteLoopWithBounds(start, end, 1, inclusive); + Assert.Equal(shouldSchedule, context.HasScheduledActivity(mockBody)); + + if (shouldSchedule) + { + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.Equal(start, currentValue); + } + } + + [Theory] + [InlineData(5, 1, 1)] // Positive step with descending range - won't execute + [InlineData(1, 5, -1)] // Negative step with ascending range - won't execute + public async Task SkipsLoopWhenInvalidConfiguration(int start, int end, int step) + { + // Arrange + var mockBody = new MockBodyActivity(); + var forActivity = new For(start, end, step) { Body = mockBody }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.False(context.HasScheduledActivity(mockBody)); + } + + [Fact] + public async Task BodyIsNull_DoesNotScheduleActivity() + { + // Arrange + var forActivity = new For(1, 5, 1) { Body = null }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + var allScheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList(); + Assert.Empty(allScheduledActivities); + } + + [Fact] + public async Task CurrentValueOutputTypeCheck_PreservesIntegerType() + { + // Arrange + var mockBody = new MockBodyActivity(); + var forActivity = new For(1, 3, 1) { Body = mockBody }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.IsType(currentValue); + Assert.Equal(1, currentValue); + } + + [Fact] + public async Task ValidConfiguration_SchedulesChildActivity() + { + // Arrange + var mockBody = new MockBodyActivity(); + var forActivity = new For(100, 102, 1) { Body = mockBody }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.True(context.HasScheduledActivity(mockBody)); + + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.Equal(100, currentValue); + } + + [Theory] + [InlineData(0, 5)] // Start with default value (0) + [InlineData(1, 0)] // End with explicit zero value + public async Task HandlesDefaultValues(int start, int end) + { + // Arrange + var mockBody = new MockBodyActivity(); + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input(1), + Body = mockBody + }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + bool shouldSchedule = ShouldExecuteLoop(start, end, 1); + Assert.Equal(shouldSchedule, context.HasScheduledActivity(mockBody)); + + if (shouldSchedule) + { + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.Equal(start, currentValue); + } + } + + [Fact] + public void VerifyActivityAttributes() + { + // Arrange + var forActivity = new For(); + var fixture = new ActivityTestFixture(forActivity); + + // Act & Assert + fixture.AssertActivityAttributes( + expectedNamespace: "Elsa", + expectedKind: ActivityKind.Action, + expectedCategory: "Looping", + expectedDisplayName: null, + expectedDescription: "Iterate over a sequence of steps between a start and an end number." + ); + } + + [Fact] + public void VerifyBreakBehaviorIsRegistered() + { + // Arrange + var forActivity = new For(); + + // Act & Assert + var breakBehavior = forActivity.Behaviors.OfType().FirstOrDefault(); + Assert.NotNull(breakBehavior); + } + + [Fact] + public void DefaultPropertyValues() + { + // Arrange + var forActivity = new For(); + + // Act & Assert + Assert.NotNull(forActivity.Start); + Assert.NotNull(forActivity.End); + Assert.NotNull(forActivity.Step); + Assert.NotNull(forActivity.OuterBoundInclusive); + } + + [Theory] + [InlineData(1, 3, 1, true)] + [InlineData(3, 1, -1, true)] + [InlineData(5, 5, 1, true)] + [InlineData(1, 5, 0, true)] // Zero step - actually schedules activity (infinite loop potential) + [InlineData(5, 1, 1, false)] // Wrong direction + [InlineData(1, 5, -1, false)] // Wrong direction + public async Task LoopDecisionLogic_ValidatesCorrectly(int start, int end, int step, bool shouldExecute) + { + // Arrange + var mockBody = new MockBodyActivity(); + var forActivity = new For(start, end, step) { Body = mockBody }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.Equal(shouldExecute, context.HasScheduledActivity(mockBody)); + } + + // Private helper methods + private static bool ShouldExecuteLoop(int start, int end, int step) + { + return ShouldExecuteLoopWithBounds(start, end, step, true); + } + + private static bool ShouldExecuteLoopWithBounds(int start, int end, int step, bool inclusive) + { + // Match the actual For activity logic exactly + var increment = step >= 0; + var currentValue = start; + + return increment && inclusive ? currentValue <= end + : increment && !inclusive ? currentValue < end + : !increment && inclusive ? currentValue >= end + : !increment && !inclusive && currentValue > end; + } + + /// + /// Mock activity to represent the body of the For loop + /// + private class MockBodyActivity : Activity + { + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + return ValueTask.CompletedTask; + } + } +} From b3ebf90f51cb646af42733b0a29418e7eabf31a4 Mon Sep 17 00:00:00 2001 From: lukhipolito-nexxbiz Date: Tue, 21 Oct 2025 17:03:19 +0200 Subject: [PATCH 2/6] Update test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs index 2dca772eb..e68c38320 100644 --- a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs @@ -239,7 +239,7 @@ public class ForTests return increment && inclusive ? currentValue <= end : increment && !inclusive ? currentValue < end : !increment && inclusive ? currentValue >= end - : !increment && !inclusive && currentValue > end; + : !increment && !inclusive ? currentValue > end : false; } /// From 5a3bb8ebf720288048503d690eb4ac9ff5585312 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Wed, 22 Oct 2025 16:08:58 +0200 Subject: [PATCH 3/6] Unit test coverage Write Http Response --- .../Http/WriteHttpResponseTests.cs | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 test/unit/Elsa.Activities.UnitTests/Http/WriteHttpResponseTests.cs diff --git a/test/unit/Elsa.Activities.UnitTests/Http/WriteHttpResponseTests.cs b/test/unit/Elsa.Activities.UnitTests/Http/WriteHttpResponseTests.cs new file mode 100644 index 000000000..4806751fc --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Http/WriteHttpResponseTests.cs @@ -0,0 +1,306 @@ +using System.Net; +using System.Text; +using Elsa.Http; +using Elsa.Http.ContentWriters; +using Elsa.Http.Options; +using Elsa.Http.Parsers; +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NSubstitute; + +namespace Elsa.Activities.UnitTests.Http; + +public class WriteHttpResponseTests +{ + [Theory] + [InlineData(HttpStatusCode.OK, 200)] + [InlineData(HttpStatusCode.Created, 201)] + [InlineData(HttpStatusCode.NotFound, 404)] + [InlineData(HttpStatusCode.InternalServerError, 500)] + public async Task Should_Set_Correct_Status_Code(HttpStatusCode statusCode, int expectedStatusCode) + { + // Arrange + var (activity, httpContext) = CreateWriteHttpResponseActivity(); + activity.StatusCode = new Input(statusCode); + + // Act + var context = await ExecuteActivityAsync(activity, httpContext); + + // Assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.True(context.IsCompleted); + } + + [Theory] + [InlineData("Hello World", "text/plain", "Hello World")] + [InlineData("{\"name\": \"John\"}", "application/json", "{\"name\": \"John\"}")] + [InlineData("John", "application/xml", "John")] + public async Task Should_Write_String_Content_With_Correct_Content_Type(string content, string contentType, string expectedContent) + { + // Arrange + var (activity, httpContext) = CreateWriteHttpResponseActivity(); + activity.Content = new Input(content); + activity.ContentType = new Input(contentType); + + // Act + await ExecuteActivityAsync(activity, httpContext); + + // Assert + Assert.Equal(contentType, httpContext.Response.ContentType); + var responseContent = GetResponseContent(httpContext); + Assert.Equal(expectedContent, responseContent); + } + + [Fact] + public async Task Should_Serialize_Object_To_Json_When_No_Content_Type_Specified() + { + // Arrange + var (activity, httpContext) = CreateWriteHttpResponseActivity(); + var testObject = new { Name = "John", Age = 30 }; + activity.Content = new Input(testObject); + + // Act + await ExecuteActivityAsync(activity, httpContext); + + // Assert + Assert.Equal("application/json", httpContext.Response.ContentType); + var responseContent = GetResponseContent(httpContext); + Assert.Contains("John", responseContent); + Assert.Contains("30", responseContent); + } + + [Theory] + [InlineData("Custom-Header", "CustomValue")] + [InlineData("X-Rate-Limit", "100")] + [InlineData("Cache-Control", "no-cache")] + public async Task Should_Add_Response_Headers(string headerName, string headerValue) + { + // Arrange + var (activity, httpContext) = CreateWriteHttpResponseActivity(); + var headers = new HttpHeaders { { headerName, new[] { headerValue } } }; + activity.ResponseHeaders = new Input(headers); + + // Act + await ExecuteActivityAsync(activity, httpContext); + + // Assert + Assert.True(httpContext.Response.Headers.ContainsKey(headerName)); + Assert.Equal(headerValue, httpContext.Response.Headers[headerName]); + } + + [Fact] + public async Task Should_Add_Multiple_Response_Headers() + { + // Arrange + var (activity, httpContext) = CreateWriteHttpResponseActivity(); + var headers = new HttpHeaders + { + { "Custom-Header-1", new[] { "Value1" } }, + { "Custom-Header-2", new[] { "Value2" } }, + { "X-Rate-Limit", new[] { "100" } } + }; + activity.ResponseHeaders = new Input(headers); + + // Act + await ExecuteActivityAsync(activity, httpContext); + + // Assert + Assert.True(httpContext.Response.Headers.ContainsKey("Custom-Header-1")); + Assert.True(httpContext.Response.Headers.ContainsKey("Custom-Header-2")); + Assert.True(httpContext.Response.Headers.ContainsKey("X-Rate-Limit")); + Assert.Equal("Value1", httpContext.Response.Headers["Custom-Header-1"]); + Assert.Equal("Value2", httpContext.Response.Headers["Custom-Header-2"]); + Assert.Equal("100", httpContext.Response.Headers["X-Rate-Limit"]); + } + + [Fact] + public async Task Should_Not_Write_Content_When_Status_Code_Is_NoContent() + { + // Arrange + var (activity, httpContext) = CreateWriteHttpResponseActivity(); + activity.StatusCode = new Input(HttpStatusCode.NoContent); + activity.Content = new Input("This should not be written"); + + // Act + await ExecuteActivityAsync(activity, httpContext); + + // Assert + Assert.Equal(204, httpContext.Response.StatusCode); + var responseContent = GetResponseContent(httpContext); + Assert.Empty(responseContent); + } + + [Fact] + public async Task Should_Handle_Null_Content() + { + // Arrange + var (activity, httpContext) = CreateWriteHttpResponseActivity(); + activity.Content = new Input((object?)null); + + // Act + await ExecuteActivityAsync(activity, httpContext); + + // Assert + Assert.Equal(200, httpContext.Response.StatusCode); + var responseContent = GetResponseContent(httpContext); + Assert.Empty(responseContent); + } + + [Fact] + public async Task Should_Call_CompleteAsync_When_WriteHttpResponseSynchronously_Is_True() + { + // Arrange + var activity = new WriteHttpResponse(); + var mockHttpContext = Substitute.For(); + var mockResponse = Substitute.For(); + var mockHeaders = new HeaderDictionary(); + var responseBody = new MemoryStream(); + + mockResponse.StatusCode.Returns(200); + mockResponse.Headers.Returns(mockHeaders); + mockResponse.Body.Returns(responseBody); + mockHttpContext.Response.Returns(mockResponse); + + // Act + await ExecuteActivityAsync(activity, mockHttpContext, writeResponseSynchronously: true); + + // Assert + await mockResponse.Received(1).CompleteAsync(); + } + + [Fact] + public async Task Should_Not_Call_CompleteAsync_When_WriteHttpResponseSynchronously_Is_False() + { + // Arrange + var activity = new WriteHttpResponse(); + var mockHttpContext = Substitute.For(); + var mockResponse = Substitute.For(); + var mockHeaders = new HeaderDictionary(); + var responseBody = new MemoryStream(); + + mockResponse.StatusCode.Returns(200); + mockResponse.Headers.Returns(mockHeaders); + mockResponse.Body.Returns(responseBody); + mockHttpContext.Response.Returns(mockResponse); + + // Act + await ExecuteActivityAsync(activity, mockHttpContext, writeResponseSynchronously: false); + + // Assert + await mockResponse.DidNotReceive().CompleteAsync(); + } + + [Fact] + public async Task Should_Create_Bookmark_When_No_HttpContext_Available() + { + // Arrange + var activity = new WriteHttpResponse(); + var fixture = new ActivityTestFixture(activity); + fixture.ConfigureServices(services => + { + var mockHttpContextAccessor = Substitute.For(); + mockHttpContextAccessor.HttpContext.Returns((HttpContext?)null); + services.AddSingleton(mockHttpContextAccessor); + services.AddSingleton(CreateMockHttpActivityOptions()); + AddHttpContentFactories(services); + services.AddSingleton(Substitute.For()); + }); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.False(context.IsCompleted); + var bookmarks = context.WorkflowExecutionContext.Bookmarks.ToList(); + Assert.Single(bookmarks); + } + + [Fact] + public async Task Should_Use_Default_Status_Code_When_Not_Specified() + { + // Arrange + var (activity, httpContext) = CreateWriteHttpResponseActivity(); + // StatusCode is not set, should default to OK (200) + + // Act + await ExecuteActivityAsync(activity, httpContext); + + // Assert + Assert.Equal(200, httpContext.Response.StatusCode); + } + + private static (WriteHttpResponse activity, HttpContext httpContext) CreateWriteHttpResponseActivity() + { + var activity = new WriteHttpResponse(); + var httpContext = new DefaultHttpContext + { + Response = + { + Body = new MemoryStream() + } + }; + + return (activity, httpContext); + } + + private static async Task ExecuteActivityAsync(WriteHttpResponse activity, HttpContext httpContext, bool writeResponseSynchronously = false) + { + var fixture = new ActivityTestFixture(activity); + fixture.ConfigureServices(services => + { + var mockHttpContextAccessor = Substitute.For(); + mockHttpContextAccessor.HttpContext.Returns(httpContext); + services.AddSingleton(mockHttpContextAccessor); + services.AddSingleton(CreateMockHttpActivityOptions(writeResponseSynchronously)); + AddHttpContentFactories(services); + AddHttpContentParsers(services); + services.AddLogging(); + }); + + return await fixture.ExecuteAsync(); + } + + private static IOptions CreateMockHttpActivityOptions(bool writeResponseSynchronously = false) + { + var options = new HttpActivityOptions + { + WriteHttpResponseSynchronously = writeResponseSynchronously + }; + var mockOptions = Substitute.For>(); + mockOptions.Value.Returns(options); + return mockOptions; + } + + private static void AddHttpContentFactories(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + private static void AddHttpContentParsers(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + private static string GetResponseContent(HttpContext httpContext) + { + httpContext.Response.Body.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(httpContext.Response.Body); + return reader.ReadToEnd(); + } + + private static byte[] GetResponseBytes(HttpContext httpContext) + { + httpContext.Response.Body.Seek(0, SeekOrigin.Begin); + return ((MemoryStream)httpContext.Response.Body).ToArray(); + } +} From 845bb4a88ff7d448c561c5d19675bac7c648b76d Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 27 Oct 2025 10:43:38 +0100 Subject: [PATCH 4/6] Refactoring for improvements for unit tests --- .../Looping/ForTests.cs | 34 ++++--------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs index e68c38320..c60367e91 100644 --- a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs @@ -10,10 +10,7 @@ public class ForTests [Theory] [InlineData(1, 3, 1)] // Ascending loop [InlineData(5, 1, -1)] // Descending loop - [InlineData(10, 12, 1)] // UpdatesCurrentValueEachIteration - [InlineData(0, 4, 1)] // HandlesFloatingPointSteps equivalent - [InlineData(10, 15, 1)] // PreservesIterationCountAccuracy - [InlineData(-1, -5, -1)] // NegativeRangeAndNegativeStep + [InlineData(-1, -5, -1)] // Descending negative loop public async Task ExecutesValidLoopConfigurations_SchedulesChildActivity(int start, int end, int step) { // Arrange @@ -114,28 +111,10 @@ public class ForTests Assert.Equal(1, currentValue); } - [Fact] - public async Task ValidConfiguration_SchedulesChildActivity() - { - // Arrange - var mockBody = new MockBodyActivity(); - var forActivity = new For(100, 102, 1) { Body = mockBody }; - var fixture = new ActivityTestFixture(forActivity); - - // Act - var context = await fixture.ExecuteAsync(); - - // Assert - Assert.True(context.HasScheduledActivity(mockBody)); - - var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); - Assert.Equal(100, currentValue); - } - [Theory] - [InlineData(0, 5)] // Start with default value (0) - [InlineData(1, 0)] // End with explicit zero value - public async Task HandlesDefaultValues(int start, int end) + [InlineData(0, 5, true)] // Start with default value (0), should execute + [InlineData(1, 0, false)] // End with explicit zero value, should not execute (wrong direction) + public async Task HandlesDefaultValues(int start, int end, bool shouldExecute) { // Arrange var mockBody = new MockBodyActivity(); @@ -152,10 +131,9 @@ public class ForTests var context = await fixture.ExecuteAsync(); // Assert - bool shouldSchedule = ShouldExecuteLoop(start, end, 1); - Assert.Equal(shouldSchedule, context.HasScheduledActivity(mockBody)); + Assert.Equal(shouldExecute, context.HasScheduledActivity(mockBody)); - if (shouldSchedule) + if (shouldExecute) { var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); Assert.Equal(start, currentValue); From 0533d5ce09980e41308e83a159356ca2e8123112 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 27 Oct 2025 14:55:50 +0100 Subject: [PATCH 5/6] Refactoring and improvements of For unit tests --- .../Looping/ForTests.cs | 272 +++++++++++++++++- 1 file changed, 257 insertions(+), 15 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs index c60367e91..4428660ec 100644 --- a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs @@ -2,6 +2,7 @@ using Elsa.Testing.Shared; using Elsa.Extensions; using Elsa.Workflows; using Elsa.Workflows.Behaviors; +using Elsa.Workflows.Exceptions; namespace Elsa.Activities.UnitTests.Looping; @@ -201,23 +202,259 @@ public class ForTests // Assert Assert.Equal(shouldExecute, context.HasScheduledActivity(mockBody)); } - - // Private helper methods - private static bool ShouldExecuteLoop(int start, int end, int step) + + // Zero step with different bounds & inclusivity (current contract: schedules at least first body) + [Theory] + [InlineData(1, 5, true, true)] // within ascending range, inclusive -> schedules + [InlineData(1, 5, false, true)] // within ascending range, exclusive -> schedules (start < end) + [InlineData(5, 1, true, false)] // start > end with ascending step (step=0 treated as positive) -> no schedule + [InlineData(5, 1, false, false)] // start > end with ascending step (step=0 treated as positive) -> no schedule + [InlineData(6, 5, true, false)] // start already outside ascending range -> no schedule + [InlineData(0, -1, false, false)]// start already outside descending (exclusive) -> no schedule + public async Task ZeroStep_RespectsInitialBoundCheck(int start, int end, bool inclusive, bool shouldSchedule) { - return ShouldExecuteLoopWithBounds(start, end, step, true); - } + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input(0), // zero step: current contract allows first schedule + OuterBoundInclusive = new Input(inclusive), + Body = body + }; + var fixture = new ActivityTestFixture(forActivity); + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.Equal(shouldSchedule, context.HasScheduledActivity(body)); + if (shouldSchedule) + { + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.IsType(currentValue); + Assert.Equal(start, currentValue); + } + } + + [Theory] + [InlineData(1, 5, 10)] // ascending, step too large + [InlineData(5, 1, -10)] // descending, step too large + public async Task StepLargerThanRange_StillSchedulesOnce(int start, int end, int step) + { + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For(start, end, step) { Body = body }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.True(context.HasScheduledActivity(body)); + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.Equal(start, currentValue); + } + + [Theory] + [InlineData(5, 5, 1)] + [InlineData(5, 5, -1)] + public async Task EqualBounds_Exclusive_DoesNotExecute(int start, int end, int step) + { + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input(step), + OuterBoundInclusive = new Input(false), + Body = body + }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.False(context.HasScheduledActivity(body)); + } + + [Theory] + [InlineData(3, 1, -1, true, true)] // inclusive: start (3) >= end (1) -> schedules + [InlineData(3, 3, -1, false, false)] // exclusive: start == end -> no schedule + [InlineData(2, 3, -1, true, false)] // start already below end for descending -> no schedule + public async Task Descending_InclusiveExclusive_OffByOne(int start, int end, int step, bool inclusive, bool shouldSchedule) + { + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input(step), + OuterBoundInclusive = new Input(inclusive), + Body = body + }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.Equal(shouldSchedule, context.HasScheduledActivity(body)); + } + + [Theory] + [InlineData(int.MaxValue, int.MaxValue, 1, true, true)] // inclusive equal -> schedules + [InlineData(int.MaxValue, int.MaxValue, 1, false, false)] // exclusive equal -> no schedule + [InlineData(int.MinValue, int.MinValue, -1, true, true)] + [InlineData(int.MinValue, int.MinValue, -1, false, false)] + public async Task ExtremeBounds_NoOverflow_OnInitialDecision(int start, int end, int step, bool inclusive, bool shouldSchedule) + { + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input(step), + OuterBoundInclusive = new Input(inclusive), + Body = body + }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.Equal(shouldSchedule, context.HasScheduledActivity(body)); + if (shouldSchedule) + { + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.Equal(start, currentValue); + } + } + + [Theory] + [InlineData("start")] + [InlineData("end")] + [InlineData("step")] + public async Task InputExpressions_Throw_DoNotSchedule(string which) + { + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For + { + Start = which == "start" ? new Input((Func)(() => throw new ApplicationException("start!"))) : new Input(1), + End = which == "end" ? new Input((Func)(() => throw new ApplicationException("end!"))) : new Input(3), + Step = which == "step" ? new Input((Func)(() => throw new ApplicationException("step!"))) : new Input(1), + Body = body + }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + Assert + var ex = await Assert.ThrowsAsync(() => fixture.ExecuteAsync()); + Assert.Contains(which, ex.Message, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task BodyThrows_ActivitySchedules_WithoutBreaking() + { + // Arrange + var body = new ThrowingBody(new InvalidOperationException("boom")); + var forActivity = new For(1, 3, 1) { Body = body }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.NotNull(context); + Assert.True(context.HasScheduledActivity(body)); + } + + [Theory] + [InlineData(true, 1, 5)] // positive step computed -> ascending executes + [InlineData(false, 5, 1)] // negative step computed -> descending executes + public async Task DynamicStep_EvaluatedAtExecutionTime(bool usePositive, int start, int end) + { + // Arrange + var body = new MockBodyActivity(); + var stepValue = usePositive ? 1 : -1; // Compute step value directly from parameter + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input((Func)(() => stepValue)), + Body = body + }; + + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.True(context.HasScheduledActivity(body)); + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.Equal(start, currentValue); + } + + [Fact] + public async Task NegativeStart_NegativeStep_CurrentValueIsIntAndMatchesStart() + { + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For(-2, -10, -2) { Body = body }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.True(context.HasScheduledActivity(body)); + var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue); + Assert.IsType(currentValue); + Assert.Equal(-2, currentValue); + } + + [Theory] + [InlineData(10, 5, 1, true)] // ascending + inclusive, start > end -> no schedule + [InlineData(10, 5, 1, false)] // ascending + exclusive, start > end -> no schedule + [InlineData(0, 5, -1, true)] // descending + inclusive, start < end -> no schedule + [InlineData(0, 5, -1, false)] // descending + exclusive, start < end -> no schedule + public async Task StartOutsideRange_DoesNotSchedule(int start, int end, int step, bool inclusive) + { + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input(step), + OuterBoundInclusive = new Input(inclusive), + Body = body + }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + var context = await fixture.ExecuteAsync(); + + // Assert + Assert.False(context.HasScheduledActivity(body)); + } + private static bool ShouldExecuteLoopWithBounds(int start, int end, int step, bool inclusive) { // Match the actual For activity logic exactly var increment = step >= 0; - var currentValue = start; - - return increment && inclusive ? currentValue <= end - : increment && !inclusive ? currentValue < end - : !increment && inclusive ? currentValue >= end - : !increment && !inclusive ? currentValue > end : false; + + return increment && inclusive ? start <= end + : increment && !inclusive ? start < end + : !increment && inclusive ? start >= end + : !increment && !inclusive && start > end; } /// @@ -225,9 +462,14 @@ public class ForTests /// private class MockBodyActivity : Activity { - protected override ValueTask ExecuteAsync(ActivityExecutionContext context) - { - return ValueTask.CompletedTask; - } + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => ValueTask.CompletedTask; + } + + /// + /// A body that throws a provided exception when executed + /// + private class ThrowingBody(Exception exception) : Activity + { + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => throw exception; } } From 8e6d51c19be123d5c87156924e58e0445cbd6512 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 27 Oct 2025 16:18:43 +0100 Subject: [PATCH 6/6] Removing unused method --- .../Http/WriteHttpResponseTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Http/WriteHttpResponseTests.cs b/test/unit/Elsa.Activities.UnitTests/Http/WriteHttpResponseTests.cs index 4806751fc..912094126 100644 --- a/test/unit/Elsa.Activities.UnitTests/Http/WriteHttpResponseTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Http/WriteHttpResponseTests.cs @@ -297,10 +297,4 @@ public class WriteHttpResponseTests using var reader = new StreamReader(httpContext.Response.Body); return reader.ReadToEnd(); } - - private static byte[] GetResponseBytes(HttpContext httpContext) - { - httpContext.Response.Body.Seek(0, SeekOrigin.Begin); - return ((MemoryStream)httpContext.Response.Body).ToArray(); - } }