From 8f35cab2adeeaf8c4503ca003fc1b3fdb51c0a87 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 27 Nov 2025 20:51:50 +0100 Subject: [PATCH] Add unit and integration tests for `Fault` activity to validate fault propagation behavior (#7113) - Introduced multiple workflows (`FaultWorkflow`, `FaultWithDefaultsWorkflow`, `FaultViaFactoryWorkflow`, `FaultInSequenceWorkflow`) to test fault scenarios. - Added `FaultTests` to verify fault propagation, stopping subsequent activities, and validating default, custom, and factory-configured fault behaviors. - Included unit tests to ensure `FaultException` is thrown correctly with all properties set and default values applied when inputs are null. --- .../Primitives/FaultTests.cs | 84 ++++++++++++ .../Workflows/FaultInSequenceWorkflow.cs | 29 ++++ .../Workflows/FaultViaFactoryWorkflow.cs | 15 ++ .../Workflows/FaultWithDefaultsWorkflow.cs | 21 +++ .../Primitives/Workflows/FaultWorkflow.cs | 21 +++ .../Primitives/FaultTests.cs | 128 ++++++++++++++++++ 6 files changed, 298 insertions(+) create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Primitives/FaultTests.cs create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultInSequenceWorkflow.cs create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultViaFactoryWorkflow.cs create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultWithDefaultsWorkflow.cs create mode 100644 test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultWorkflow.cs create mode 100644 test/unit/Elsa.Activities.UnitTests/Primitives/FaultTests.cs diff --git a/test/integration/Elsa.Activities.IntegrationTests/Primitives/FaultTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Primitives/FaultTests.cs new file mode 100644 index 000000000..46d18ceeb --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Primitives/FaultTests.cs @@ -0,0 +1,84 @@ +using Elsa.Activities.IntegrationTests.Primitives.Workflows; +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Exceptions; +using Elsa.Workflows.Models; +using Elsa.Workflows.State; +using Xunit.Abstractions; + +namespace Elsa.Activities.IntegrationTests.Primitives; + +/// +/// Integration tests for the activity. +/// +public class FaultTests(ITestOutputHelper testOutputHelper) +{ + private readonly WorkflowTestFixture _fixture = new WorkflowTestFixture(testOutputHelper) + .AddWorkflow() + .AddWorkflow() + .AddWorkflow() + .AddWorkflow(); + + [Fact(DisplayName = "Fault activity faults workflow execution")] + public async Task Fault_Activity_Faults_Workflow_Execution() + { + // Act + var workflowState = await _fixture.RunWorkflowAsync(FaultWorkflow.DefinitionId); + + // Assert + AssertWorkflowFaulted(workflowState); + var incident = GetSingleIncident(workflowState); + Assert.Equal("Test fault message", incident.Message); + } + + [Fact(DisplayName = "Fault activity with default values faults workflow")] + public async Task Fault_With_Default_Values_Faults_Workflow() + { + // Act + var workflowState = await _fixture.RunWorkflowAsync(FaultWithDefaultsWorkflow.DefinitionId); + + // Assert + AssertWorkflowFaulted(workflowState); + GetSingleIncident(workflowState); + } + + [Fact(DisplayName = "Fault in sequence stops subsequent activities")] + public async Task Fault_In_Sequence_Stops_Subsequent_Activities() + { + // Act + var workflowState = await _fixture.RunWorkflowAsync(FaultInSequenceWorkflow.DefinitionId); + + // Assert + AssertWorkflowFaulted(workflowState); + + var lines = _fixture.CapturingTextWriter.Lines.ToList(); + Assert.Contains("Before fault", lines); + Assert.DoesNotContain("After fault - should not execute", lines); + } + + [Fact(DisplayName = "Fault with factory method creates correct exception")] + public async Task Fault_Created_With_Factory_Method() + { + // Act + var workflowState = await _fixture.RunWorkflowAsync(FaultViaFactoryWorkflow.DefinitionId); + + // Assert + AssertWorkflowFaulted(workflowState); + var incident = GetSingleIncident(workflowState); + Assert.Equal("Created via factory", incident.Message); + } + + private static void AssertWorkflowFaulted(WorkflowState workflowState) + { + Assert.Equal(WorkflowSubStatus.Faulted, workflowState.SubStatus); + } + + private static ActivityIncident GetSingleIncident(WorkflowState workflowState) + { + var incident = Assert.Single(workflowState.Incidents); + Assert.NotNull(incident.Exception); + Assert.Equal(typeof(FaultException), incident.Exception.Type); + return incident; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultInSequenceWorkflow.cs b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultInSequenceWorkflow.cs new file mode 100644 index 000000000..c19f21756 --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultInSequenceWorkflow.cs @@ -0,0 +1,29 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; + +namespace Elsa.Activities.IntegrationTests.Primitives.Workflows; + +public class FaultInSequenceWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = nameof(FaultInSequenceWorkflow); + + protected override void Build(IWorkflowBuilder workflow) + { + workflow.DefinitionId = DefinitionId; + workflow.Root = new Sequence + { + Activities = + { + new WriteLine("Before fault"), + new Fault + { + Code = new("SEQ_001"), + Category = new("Sequence"), + FaultType = new("Test"), + Message = new("Fault in sequence") + }, + new WriteLine("After fault - should not execute") + } + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultViaFactoryWorkflow.cs b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultViaFactoryWorkflow.cs new file mode 100644 index 000000000..efe5e0321 --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultViaFactoryWorkflow.cs @@ -0,0 +1,15 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; + +namespace Elsa.Activities.IntegrationTests.Primitives.Workflows; + +public class FaultViaFactoryWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = nameof(FaultViaFactoryWorkflow); + + protected override void Build(IWorkflowBuilder workflow) + { + workflow.DefinitionId = DefinitionId; + workflow.Root = Fault.Create("FACTORY_001", "Factory", "Integration", "Created via factory"); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultWithDefaultsWorkflow.cs b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultWithDefaultsWorkflow.cs new file mode 100644 index 000000000..9b7cb618a --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultWithDefaultsWorkflow.cs @@ -0,0 +1,21 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; + +namespace Elsa.Activities.IntegrationTests.Primitives.Workflows; + +public class FaultWithDefaultsWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = nameof(FaultWithDefaultsWorkflow); + + protected override void Build(IWorkflowBuilder workflow) + { + workflow.DefinitionId = DefinitionId; + workflow.Root = new Fault + { + Code = new((string)null!), + Category = new((string)null!), + FaultType = new((string)null!), + Message = new((string?)null) + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultWorkflow.cs b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultWorkflow.cs new file mode 100644 index 000000000..32053fab7 --- /dev/null +++ b/test/integration/Elsa.Activities.IntegrationTests/Primitives/Workflows/FaultWorkflow.cs @@ -0,0 +1,21 @@ +using Elsa.Workflows; +using Elsa.Workflows.Activities; + +namespace Elsa.Activities.IntegrationTests.Primitives.Workflows; + +public class FaultWorkflow : WorkflowBase +{ + public static readonly string DefinitionId = nameof(FaultWorkflow); + + protected override void Build(IWorkflowBuilder workflow) + { + workflow.DefinitionId = DefinitionId; + workflow.Root = new Fault + { + Code = new("ERR_001"), + Category = new("Test"), + FaultType = new("Business"), + Message = new("Test fault message") + }; + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Activities.UnitTests/Primitives/FaultTests.cs b/test/unit/Elsa.Activities.UnitTests/Primitives/FaultTests.cs new file mode 100644 index 000000000..179b8923b --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Primitives/FaultTests.cs @@ -0,0 +1,128 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Elsa.Workflows.Exceptions; + +namespace Elsa.Activities.UnitTests.Primitives; + +/// +/// Unit tests for the activity. +/// +public class FaultTests +{ + [Fact(DisplayName = "Fault throws FaultException with all properties set")] + public async Task Should_Throw_FaultException_With_All_Properties() + { + // Arrange + var fault = new Fault + { + Code = new("ERR_001"), + Category = new("HTTP"), + FaultType = new("Business"), + Message = new("Invalid request") + }; + + // Act & Assert + var exception = await ExecuteAndAssertFaultAsync(fault); + Assert.Equal("ERR_001", exception.Code); + Assert.Equal("HTTP", exception.Category); + Assert.Equal("Business", exception.Type); + Assert.Equal("Invalid request", exception.Message); + } + + [Fact(DisplayName = "Fault uses default values when inputs are null")] + public async Task Should_Use_Default_Values_When_Inputs_Are_Null() + { + // Arrange + var fault = new Fault + { + Code = new((string)null!), + Category = new((string)null!), + FaultType = new((string)null!), + Message = new((string?)null) + }; + + // Act & Assert + var exception = await ExecuteAndAssertFaultAsync(fault); + Assert.Equal("0", exception.Code); + Assert.Equal("General", exception.Category); + Assert.Equal("System", exception.Type); + Assert.NotNull(exception.Message); + } + + [Fact(DisplayName = "Fault throws FaultException with null message")] + public async Task Should_Throw_FaultException_With_Null_Message() + { + // Arrange + var fault = new Fault + { + Code = new("ERR_002"), + Category = new("Database"), + FaultType = new("Integration"), + Message = new((string?)null) + }; + + // Act & Assert + var exception = await ExecuteAndAssertFaultAsync(fault); + Assert.Equal("ERR_002", exception.Code); + Assert.Equal("Database", exception.Category); + Assert.Equal("Integration", exception.Type); + Assert.NotNull(exception.Message); + } + + [Theory(DisplayName = "Fault throws FaultException with various code values")] + [InlineData("404", "404")] + [InlineData("VALIDATION_ERROR", "VALIDATION_ERROR")] + [InlineData("", "")] + public async Task Should_Throw_FaultException_With_Various_Codes(string code, string expectedCode) + { + // Arrange + var fault = new Fault + { + Code = new(code), + Category = new("Test"), + FaultType = new("Test") + }; + + // Act & Assert + var exception = await ExecuteAndAssertFaultAsync(fault); + Assert.Equal(expectedCode, exception.Code); + } + + [Fact(DisplayName = "Fault.Create factory method creates correctly configured instance")] + public async Task Create_Factory_Method_Creates_Correctly_Configured_Instance() + { + // Arrange + var fault = Fault.Create("CREATE_001", "Factory", "Test", "Created via factory"); + + // Act & Assert + var exception = await ExecuteAndAssertFaultAsync(fault); + Assert.Equal("CREATE_001", exception.Code); + Assert.Equal("Factory", exception.Category); + Assert.Equal("Test", exception.Type); + Assert.Equal("Created via factory", exception.Message); + } + + [Fact(DisplayName = "Fault.Create factory method with null message")] + public async Task Create_Factory_Method_With_Null_Message() + { + // Arrange + var fault = Fault.Create("CODE", "Category", "Type"); + + // Act & Assert + var exception = await ExecuteAndAssertFaultAsync(fault); + Assert.Equal("CODE", exception.Code); + Assert.Equal("Category", exception.Category); + Assert.Equal("Type", exception.Type); + Assert.NotNull(exception.Message); + } + + private static async Task ExecuteAndAssertFaultAsync(IActivity activity) + { + return await Assert.ThrowsAsync(() => ExecuteAsync(activity)); + } + + private static async Task ExecuteAsync(IActivity activity) + { + return await new ActivityTestFixture(activity).ExecuteAsync(); + } +}