diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs index 901be6eda..618bbfd03 100644 --- a/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/DryRun/Endpoint.cs @@ -1,7 +1,6 @@ using Elsa.Abstractions; using Elsa.Alterations.Core.Contracts; using Elsa.Alterations.Core.Models; -using Elsa.Workflows.Management.Filters; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; @@ -36,11 +35,6 @@ public class DryRun(IWorkflowInstanceFinder workflowInstanceFinder) : ElsaEndpoi private bool ValidateInput(AlterationWorkflowInstanceFilter filter) { - var errors = WorkflowInstanceFilter.ValidateTimestampFilters(filter.TimestampFilters).ToList(); - - foreach (var error in errors) - AddError(error); - - return errors.Count == 0; + return TimestampFilterValidation.Validate(filter.TimestampFilters, error => AddError(error)); } } diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs index 8b2319f1e..43e08d53f 100644 --- a/src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/Submit/Endpoint.cs @@ -3,7 +3,6 @@ using Elsa.Alterations.Core.Contracts; using Elsa.Alterations.Core.Models; using Elsa.Common; using Elsa.Workflows; -using Elsa.Workflows.Management.Filters; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; @@ -53,11 +52,6 @@ public class Submit : ElsaEndpoint private bool ValidateInput(AlterationPlanParams planParams) { - var errors = WorkflowInstanceFilter.ValidateTimestampFilters(planParams.Filter?.TimestampFilters).ToList(); - - foreach (var error in errors) - AddError(error); - - return errors.Count == 0; + return TimestampFilterValidation.Validate(planParams.Filter?.TimestampFilters, error => AddError(error)); } } diff --git a/src/modules/Elsa.Alterations/Endpoints/Alterations/TimestampFilterValidation.cs b/src/modules/Elsa.Alterations/Endpoints/Alterations/TimestampFilterValidation.cs new file mode 100644 index 000000000..900bf896e --- /dev/null +++ b/src/modules/Elsa.Alterations/Endpoints/Alterations/TimestampFilterValidation.cs @@ -0,0 +1,20 @@ +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Management.Models; + +namespace Elsa.Alterations.Endpoints.Alterations; + +internal static class TimestampFilterValidation +{ + public static bool Validate(IEnumerable? timestampFilters, Action addError) + { + var isValid = true; + + foreach (var error in WorkflowInstanceFilter.ValidateTimestampFilters(timestampFilters)) + { + addError(error); + isValid = false; + } + + return isValid; + } +} diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs index 8aa9f0f46..51e82e7ae 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/List/Endpoint.cs @@ -84,12 +84,15 @@ internal class List(IWorkflowInstanceStore store) : ElsaEndpoint> FindAsync(Request request, WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken) diff --git a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs index 0157107ca..ae390143f 100644 --- a/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs +++ b/src/modules/Elsa.Workflows.Management/Filters/WorkflowInstanceFilter.cs @@ -166,6 +166,9 @@ public class WorkflowInstanceFilter { foreach (var timestampFilter in TimestampFilters) { + if (timestampFilter == null) + throw new ArgumentException("Timestamp filter must be specified.", nameof(TimestampFilters)); + var column = NormalizeTimestampFilterColumn(timestampFilter.Column); var timestamp = timestampFilter.Timestamp; var isZeroTime = timestamp.TimeOfDay == TimeSpan.Zero; diff --git a/test/integration/Elsa.Alterations.IntegrationTests/AlterationsApiTimestampFilterTests.cs b/test/integration/Elsa.Alterations.IntegrationTests/AlterationsApiTimestampFilterTests.cs index 2b1b93a39..c1f6b42e4 100644 --- a/test/integration/Elsa.Alterations.IntegrationTests/AlterationsApiTimestampFilterTests.cs +++ b/test/integration/Elsa.Alterations.IntegrationTests/AlterationsApiTimestampFilterTests.cs @@ -98,6 +98,19 @@ public class AlterationsApiTimestampFilterTests : IAsyncLifetime Assert.Contains("Timestamp filter must be specified.", body); } + [Theory] + [InlineData("/alterations/dry-run")] + [InlineData("/alterations/submit")] + public async Task Post_WithMultipleInvalidTimestampFilters_ReturnsAllValidationErrors(string path) + { + var response = await _httpClient.PostAsJsonAsync(path, CreateRequestWithMultipleInvalidTimestampFilters(path)); + var body = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains("Timestamp filter column must be specified.", body); + Assert.Contains("Timestamp filter must be specified.", body); + } + private static object CreateRequest(string path) { var filter = new @@ -129,4 +142,25 @@ public class AlterationsApiTimestampFilterTests : IAsyncLifetime ? new { filter } : filter; } + + private static object CreateRequestWithMultipleInvalidTimestampFilters(string path) + { + var filter = new + { + timestampFilters = new object?[] + { + new + { + column = " ", + @operator = TimestampFilterOperator.Is, + timestamp = new DateTimeOffset(2026, 5, 20, 10, 0, 0, TimeSpan.Zero) + }, + null + } + }; + + return path == "/alterations/submit" + ? new { filter } + : filter; + } } diff --git a/test/unit/Elsa.Workflows.Management.UnitTests/Filters/WorkflowInstanceFilterTimestampTests.cs b/test/unit/Elsa.Workflows.Management.UnitTests/Filters/WorkflowInstanceFilterTimestampTests.cs index 4d4468474..b60d6ed42 100644 --- a/test/unit/Elsa.Workflows.Management.UnitTests/Filters/WorkflowInstanceFilterTimestampTests.cs +++ b/test/unit/Elsa.Workflows.Management.UnitTests/Filters/WorkflowInstanceFilterTimestampTests.cs @@ -70,6 +70,20 @@ public class WorkflowInstanceFilterTimestampTests Assert.Contains("CreatedAt, UpdatedAt, FinishedAt", exception.Message); } + [Fact] + public void Apply_WithNullTimestampFilter_ThrowsClearArgumentException() + { + var filter = new WorkflowInstanceFilter + { + TimestampFilters = [null!] + }; + + var exception = Assert.Throws(() => filter.Apply(_workflowInstances).ToList()); + + Assert.Contains("Timestamp filter must be specified.", exception.Message); + Assert.Equal(nameof(WorkflowInstanceFilter.TimestampFilters), exception.ParamName); + } + [Fact] public void ValidateTimestampFilters_WithMissingColumn_ReturnsClearValidationError() {