diff --git a/src/modules/Elsa.Http/Features/HttpFeature.cs b/src/modules/Elsa.Http/Features/HttpFeature.cs index 80e0e5120..322e6dfc4 100644 --- a/src/modules/Elsa.Http/Features/HttpFeature.cs +++ b/src/modules/Elsa.Http/Features/HttpFeature.cs @@ -3,6 +3,7 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; +using Elsa.Http.Bookmarks; using Elsa.Http.ContentWriters; using Elsa.Http.DownloadableContentHandlers; using Elsa.Http.FileCaches; @@ -14,6 +15,7 @@ using Elsa.Http.Resilience; using Elsa.Http.Selectors; using Elsa.Http.Services; using Elsa.Http.Tasks; +using Elsa.Http.TriggerPayloadValidators; using Elsa.Http.UIHints; using Elsa.Resilience.Extensions; using Elsa.Resilience.Features; @@ -172,7 +174,6 @@ public class HttpFeature(IModule module) : FeatureBase(module) .AddHttpContextAccessor() // Handlers. - .AddNotificationHandler() .AddNotificationHandler() // Content parsers. @@ -220,6 +221,9 @@ public class HttpFeature(IModule module) : FeatureBase(module) .AddScoped() .AddScoped() + //Trigger payload validators. + .AddTriggerPaylodValidator() + // File caches. .AddScoped(FileCache) .AddScoped() diff --git a/src/modules/Elsa.Http/Handlers/ValidateWorkflowRequestHandler.cs b/src/modules/Elsa.Http/Handlers/ValidateWorkflowRequestHandler.cs deleted file mode 100644 index fb51064e7..000000000 --- a/src/modules/Elsa.Http/Handlers/ValidateWorkflowRequestHandler.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Elsa.Extensions; -using Elsa.Http.Bookmarks; -using Elsa.Mediator.Contracts; -using Elsa.Workflows.Helpers; -using Elsa.Workflows.Management.Notifications; -using Elsa.Workflows.Runtime; -using Elsa.Workflows.Runtime.Filters; -using JetBrains.Annotations; - -namespace Elsa.Http.Handlers; - -/// -/// A handler that validates a workflow path and return any errors. -/// -[UsedImplicitly] -public class ValidateWorkflowRequestHandler : INotificationHandler -{ - private readonly ITriggerStore _triggerStore; - private readonly ITriggerIndexer _triggerIndexer; - - /// - /// Constructor. - /// - public ValidateWorkflowRequestHandler(ITriggerStore triggerStore, ITriggerIndexer triggerIndexer) - { - _triggerStore = triggerStore; - _triggerIndexer = triggerIndexer; - } - - public async Task HandleAsync(WorkflowDefinitionValidating notification, CancellationToken cancellationToken) - { - var workflow = notification.Workflow; - var httpEndpointTriggers = (await _triggerIndexer.GetTriggersAsync(workflow, cancellationToken)).Where(x => x.Payload is HttpEndpointBookmarkPayload).ToList(); - var filter = new TriggerFilter - { - Name = ActivityTypeNameHelper.GenerateTypeName(typeof(HttpEndpoint)) - }; - var publishedWorkflowsTriggers = (await _triggerStore.FindManyAsync(filter, cancellationToken)).ToList(); - var validationErrors = notification.ValidationErrors; - - foreach (var httpEndpointTrigger in httpEndpointTriggers) - { - var triggerPayload = httpEndpointTrigger.GetPayload(); - - var otherWorkflowsWithSamePath = publishedWorkflowsTriggers - .Where(x => - x.WorkflowDefinitionId != workflow.Identity.DefinitionId && - x.Payload is HttpEndpointBookmarkPayload payload && - payload.Path == triggerPayload.Path && - payload.Method == triggerPayload.Method) - .ToList(); - - if (!otherWorkflowsWithSamePath.Any()) - continue; - - var message = $"The {triggerPayload.Path} path and {triggerPayload.Method} method are already in use by another workflow!"; - validationErrors.Add(new(message, httpEndpointTrigger.ActivityId)); - } - } -} diff --git a/src/modules/Elsa.Http/TriggerPayloadValidators/HttpEndpointTriggerPayloadValidator.cs b/src/modules/Elsa.Http/TriggerPayloadValidators/HttpEndpointTriggerPayloadValidator.cs new file mode 100644 index 000000000..5d2c0d8bd --- /dev/null +++ b/src/modules/Elsa.Http/TriggerPayloadValidators/HttpEndpointTriggerPayloadValidator.cs @@ -0,0 +1,41 @@ +using Elsa.Http.Bookmarks; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Helpers; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Entities; +using Elsa.Workflows.Runtime.Filters; + +namespace Elsa.Http.TriggerPayloadValidators; + +public class HttpEndpointTriggerPayloadValidator(ITriggerStore triggerStore) : ITriggerPayloadValidator +{ + public async Task ValidateAsync( + HttpEndpointBookmarkPayload payload, + Workflow workflow, + StoredTrigger trigger, + ICollection validationErrors, + CancellationToken cancellationToken) + { + var filter = new TriggerFilter + { + Name = ActivityTypeNameHelper.GenerateTypeName(typeof(HttpEndpoint)) + }; + var publishedWorkflowsTriggers = (await triggerStore.FindManyAsync(filter, cancellationToken)).ToList(); + + var otherWorkflowsWithSamePath = publishedWorkflowsTriggers + .Where(x => + x.WorkflowDefinitionId != workflow.Identity.DefinitionId && + x.Payload is HttpEndpointBookmarkPayload anotherHttpEndpointPayload && + anotherHttpEndpointPayload.Path == payload.Path && + anotherHttpEndpointPayload.Method == payload.Method) + .ToList(); + + if (otherWorkflowsWithSamePath.Count == 0) + return; + + validationErrors.Add(new($"The {payload.Path} path and {payload.Method} method are already in use by another workflow!", + trigger.ActivityId)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Scheduling/Bookmarks/CronTriggerPayload.cs b/src/modules/Elsa.Scheduling/Bookmarks/CronTriggerPayload.cs index 52d742657..fc3c3c62f 100644 --- a/src/modules/Elsa.Scheduling/Bookmarks/CronTriggerPayload.cs +++ b/src/modules/Elsa.Scheduling/Bookmarks/CronTriggerPayload.cs @@ -1,3 +1,3 @@ namespace Elsa.Scheduling.Bookmarks; -internal record CronTriggerPayload(string CronExpression); \ No newline at end of file +public record CronTriggerPayload(string CronExpression); \ No newline at end of file diff --git a/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs b/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs index 7ef3dfdec..41eb4b5eb 100644 --- a/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs +++ b/src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs @@ -4,9 +4,11 @@ using Elsa.Extensions; using Elsa.Features.Abstractions; using Elsa.Features.Attributes; using Elsa.Features.Services; +using Elsa.Scheduling.Bookmarks; using Elsa.Scheduling.Handlers; using Elsa.Scheduling.HostedServices; using Elsa.Scheduling.Services; +using Elsa.Scheduling.TriggerPayloadValidators; using Elsa.Workflows.Management.Features; using Microsoft.Extensions.DependencyInjection; @@ -49,7 +51,10 @@ public class SchedulingFeature : FeatureBase .AddSingleton(CronParser) .AddScoped(WorkflowScheduler) .AddBackgroundTask() - .AddHandlersFrom(); + .AddHandlersFrom() + + //Trigger payload validators. + .AddTriggerPaylodValidator(); Module.Configure(management => management.AddActivitiesFrom()); } diff --git a/src/modules/Elsa.Scheduling/TriggerPayloadValidators/CronTriggerPayloadValidator.cs b/src/modules/Elsa.Scheduling/TriggerPayloadValidators/CronTriggerPayloadValidator.cs new file mode 100644 index 000000000..7b3478af5 --- /dev/null +++ b/src/modules/Elsa.Scheduling/TriggerPayloadValidators/CronTriggerPayloadValidator.cs @@ -0,0 +1,30 @@ +using Cronos; +using Elsa.Scheduling.Bookmarks; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Entities; + +namespace Elsa.Scheduling.TriggerPayloadValidators; + +public class CronTriggerPayloadValidator(ICronParser cronParser) : ITriggerPayloadValidator +{ + public Task ValidateAsync( + CronTriggerPayload payload, + Workflow workflow, + StoredTrigger trigger, + ICollection validationErrors, + CancellationToken cancellationToken) + { + try + { + cronParser.GetNextOccurrence(payload.CronExpression); + } + catch (CronFormatException ex) + { + validationErrors.Add(new("Error when parsing cron expression: " + ex.Message, + trigger.ActivityId)); + } + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/ITriggerPayloadValidator.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/ITriggerPayloadValidator.cs new file mode 100644 index 000000000..99600c0ba --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/ITriggerPayloadValidator.cs @@ -0,0 +1,21 @@ +using Elsa.Workflows.Activities; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Runtime.Entities; + +namespace Elsa.Workflows.Runtime.Contracts; + +/// +/// Validator that validate a given trigger payload. +/// +public interface ITriggerPayloadValidator +{ + /// + /// Validate a trigger payload. If trigger is not valid, error will be add in this list. + /// + Task ValidateAsync( + TPayload payload, + Workflow workflow, + StoredTrigger trigger, + ICollection validationErrors, + CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/DependencyInjectionExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/DependencyInjectionExtensions.cs index 31e29f8f8..c7354560e 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/DependencyInjectionExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/DependencyInjectionExtensions.cs @@ -1,4 +1,5 @@ using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Contracts; using Elsa.Workflows.Runtime.Providers; // ReSharper disable once CheckNamespace @@ -16,4 +17,16 @@ public static class DependencyInjectionExtensions /// The type of the workflow definition provider. /// The service collection. public static IServiceCollection AddWorkflowDefinitionProvider(this IServiceCollection services) where T : class, IWorkflowsProvider => services.AddScoped(); + + /// + /// Registers a with the service container. + /// + /// Service collection + /// Validator of the validator + /// Payload type + public static IServiceCollection AddTriggerPaylodValidator(this IServiceCollection services) + where TValidator : class, ITriggerPayloadValidator + { + return services.AddScoped, TValidator>(); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index 7566d57a0..78ba10a62 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -345,6 +345,7 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module) .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() + .AddNotificationHandler() // Workflow activation strategies. .AddScoped() diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/ValidateWorkflowRequestHandler.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/ValidateWorkflowRequestHandler.cs new file mode 100644 index 000000000..26fd3957a --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/ValidateWorkflowRequestHandler.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Management.Notifications; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Entities; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Workflows.Runtime.Handlers; + +/// +/// A handler that validates a workflow path and return any errors. +/// +[UsedImplicitly] +public class ValidateWorkflowRequestHandler( + ITriggerIndexer triggerIndexer, + IServiceProvider serviceProvider) : INotificationHandler +{ + public async Task HandleAsync(WorkflowDefinitionValidating notification, CancellationToken cancellationToken) + { + Workflow workflow = notification.Workflow; + ICollection validationErrors = notification.ValidationErrors; + foreach (StoredTrigger trigger in await triggerIndexer.GetTriggersAsync(workflow, cancellationToken)) + { + if (trigger.Payload is null) + { + validationErrors.Add(new($"Trigger should have a payload", + trigger.ActivityId)); + } + else + { + MethodInfo method = typeof(ValidateWorkflowRequestHandler) + .GetMethod(nameof(ValidateInternalAsync), BindingFlags.Instance | BindingFlags.NonPublic)! + .MakeGenericMethod(trigger.Payload.GetType()); + + object? result = method.Invoke(this, [trigger.Payload, workflow, trigger, validationErrors, cancellationToken]); + if (result is Task task) + { + await task; + } + } + } + } + + private async Task ValidateInternalAsync(TPayload instance, + Workflow workflow, + StoredTrigger trigger, + ICollection validationErrors, + CancellationToken cancellationToken) + { + ITriggerPayloadValidator? payloadValidator = serviceProvider.GetService>(); + + if (payloadValidator != null) + await payloadValidator.ValidateAsync(instance, workflow, trigger, validationErrors, cancellationToken); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishCronTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishCronTests.cs new file mode 100644 index 000000000..e61fa08b1 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishCronTests.cs @@ -0,0 +1,56 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows.Management; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.ImportAndPublish; + +public class ImportAndPublishCronTests +{ + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IServiceProvider _services; + + public ImportAndPublishCronTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .Build(); + } + + [Fact(DisplayName = "Cron workflow imported from file should publish successfully.")] + public async Task ImportAndPublish_ShouldSucceed_WithGoodCron() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import workflow. + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/cron-every-hour.json"); + + // Publish. + IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService(); + var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition); + + // Assert. + Assert.True(result.Succeeded); + Assert.Empty(result.ValidationErrors); + } + + [Fact(DisplayName = "Cron workflow imported from file should not publish successfully with bad cron expression.")] + public async Task ImportAndPublish_ShouldFailed_WithBadCronExpression() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import workflow. + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/bad-cron-expression.json"); + + // Publish. + IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService(); + var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition); + + // Assert. + Assert.False(result.Succeeded); + Assert.Single(result.ValidationErrors); + Assert.Equal("Error when parsing cron expression: The given cron expression has an invalid format. Seconds: Value must be a number between 0 and 59 (all inclusive).", result.ValidationErrors.Single().Message); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishHttpEndpointsTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishHttpEndpointsTests.cs new file mode 100644 index 000000000..d1b5a322e --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishHttpEndpointsTests.cs @@ -0,0 +1,68 @@ +using Elsa.Extensions; +using Elsa.Testing.Shared; +using Elsa.Workflows.Management; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.ImportAndPublish; + +public class ImportAndPublishHttpEndpointsTests +{ + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IServiceProvider _services; + + public ImportAndPublishHttpEndpointsTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .ConfigureElsa(configure => configure.UseHttp()) + .Build(); + } + + [Fact(DisplayName = "Http endpoint workflow imported from file should publish successfully.")] + public async Task ImportAndPublish_ShouldSucceed_WithGoodHttpEndpoint() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import workflow. + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/http-workflow.json"); + + // Publish. + IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService(); + var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition); + + // Assert. + Assert.True(result.Succeeded); + Assert.Empty(result.ValidationErrors); + } + + [Fact(DisplayName = "Http endpoint workflow imported from file should not publish successfully with same path and method.")] + public async Task ImportAndPublish_ShouldFailed_WithTwoHttpEndpointSamePathMethod() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import first workflow. + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/http-workflow.json"); + + // Publish first workflow. + IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService(); + var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition); + + // Assert first workflow. + Assert.True(result.Succeeded); + Assert.Empty(result.ValidationErrors); + + // Import second workflow. + workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/http-workflow.json"); + + // Publish second workflow. + result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition); + + // Assert second workflow. + Assert.False(result.Succeeded); + Assert.Single(result.ValidationErrors); + Assert.Equal("The /test path and get method are already in use by another workflow!", result.ValidationErrors.Single().Message); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishTimerTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishTimerTests.cs new file mode 100644 index 000000000..e64442703 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/ImportAndPublishTimerTests.cs @@ -0,0 +1,37 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows.Management; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.ImportAndPublish; + +public class ImportAndPublishTimerTests +{ + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IServiceProvider _services; + + public ImportAndPublishTimerTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .Build(); + } + + [Fact(DisplayName = "Timer workflow imported from file should publish successfully.")] + public async Task ImportAndPublish_ShouldSucceed_WithGoodTimerWithoutValidator() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import workflow. + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/ImportAndPublish/Workflows/timer-workflow.json"); + + // Publish. + IWorkflowDefinitionPublisher workflowDefinitionPublisher = _services.GetRequiredService(); + var result = await workflowDefinitionPublisher.PublishAsync(workflowDefinition); + + // Assert. + Assert.True(result.Succeeded); + Assert.Empty(result.ValidationErrors); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/bad-cron-expression.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/bad-cron-expression.json new file mode 100644 index 000000000..dc7ddc495 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/bad-cron-expression.json @@ -0,0 +1,117 @@ +{ + "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", + "id": "868f0fa68d846638", + "definitionId": "311cedd544cfe691", + "name": "CronWorkflow", + "createdAt": "2025-05-12T12:16:06.603274+00:00", + "version": 1, + "toolVersion": "3.3.0.0", + "variables": [ + ], + "inputs": [ + ], + "outputs": [ + ], + "outcomes": [ + ], + "customProperties": { + }, + "isReadonly": false, + "isSystem": false, + "isLatest": true, + "isPublished": false, + "options": { + "autoUpdateConsumingWorkflows": false + }, + "root": { + "id": "57363d0e76c3e9d2", + "nodeId": "Workflow1:57363d0e76c3e9d2", + "name": "Flowchart1", + "type": "Elsa.Flowchart", + "version": 1, + "customProperties": { + "notFoundConnections": [ + ], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + }, + "activities": [ + { + "text": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "Hello every hour" + } + }, + "id": "1bf2412c9603f5d3", + "nodeId": "Workflow1:57363d0e76c3e9d2:1bf2412c9603f5d3", + "name": "WriteLine1", + "type": "Elsa.WriteLine", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -39.899993896484375, + "y": -88.600006103515625 + }, + "size": { + "width": 138.02500915527344, + "height": 49.600002288818359 + } + } + } + }, + { + "cronExpression": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "BadCronExpression" + } + }, + "id": "13658e4b3bfafaaf", + "nodeId": "Workflow1:57363d0e76c3e9d2:13658e4b3bfafaaf", + "name": "Cron1", + "type": "Elsa.Cron", + "version": 1, + "customProperties": { + "canStartWorkflow": true, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -206.89999389648438, + "y": -88.600006103515625 + }, + "size": { + "width": 100.59999847412109, + "height": 49.600002288818359 + } + } + } + } + ], + "variables": [ + ], + "connections": [ + { + "source": { + "activity": "13658e4b3bfafaaf", + "port": "Done" + }, + "target": { + "activity": "1bf2412c9603f5d3", + "port": "In" + } + } + ] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/cron-every-hour.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/cron-every-hour.json new file mode 100644 index 000000000..e0b779010 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/cron-every-hour.json @@ -0,0 +1,117 @@ +{ + "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", + "id": "868f0fa68d846638", + "definitionId": "311cedd544cfe691", + "name": "CronWorkflow", + "createdAt": "2025-05-12T12:16:06.603274+00:00", + "version": 1, + "toolVersion": "3.3.0.0", + "variables": [ + ], + "inputs": [ + ], + "outputs": [ + ], + "outcomes": [ + ], + "customProperties": { + }, + "isReadonly": false, + "isSystem": false, + "isLatest": true, + "isPublished": false, + "options": { + "autoUpdateConsumingWorkflows": false + }, + "root": { + "id": "57363d0e76c3e9d2", + "nodeId": "Workflow1:57363d0e76c3e9d2", + "name": "Flowchart1", + "type": "Elsa.Flowchart", + "version": 1, + "customProperties": { + "notFoundConnections": [ + ], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + }, + "activities": [ + { + "text": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "Hello every hour" + } + }, + "id": "1bf2412c9603f5d3", + "nodeId": "Workflow1:57363d0e76c3e9d2:1bf2412c9603f5d3", + "name": "WriteLine1", + "type": "Elsa.WriteLine", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -39.899993896484375, + "y": -88.600006103515625 + }, + "size": { + "width": 138.02500915527344, + "height": 49.600002288818359 + } + } + } + }, + { + "cronExpression": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "0 0 * * * *" + } + }, + "id": "13658e4b3bfafaaf", + "nodeId": "Workflow1:57363d0e76c3e9d2:13658e4b3bfafaaf", + "name": "Cron1", + "type": "Elsa.Cron", + "version": 1, + "customProperties": { + "canStartWorkflow": true, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -206.89999389648438, + "y": -88.600006103515625 + }, + "size": { + "width": 100.59999847412109, + "height": 49.600002288818359 + } + } + } + } + ], + "variables": [ + ], + "connections": [ + { + "source": { + "activity": "13658e4b3bfafaaf", + "port": "Done" + }, + "target": { + "activity": "1bf2412c9603f5d3", + "port": "In" + } + } + ] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/http-workflow.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/http-workflow.json new file mode 100644 index 000000000..36b11b6f3 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/http-workflow.json @@ -0,0 +1,151 @@ +{ + "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", + "name": "HttpWorkflow", + "createdAt": "2025-05-12T12:28:56.845918+00:00", + "version": 1, + "toolVersion": "3.3.0.0", + "variables": [ + ], + "inputs": [ + ], + "outputs": [ + ], + "outcomes": [ + ], + "customProperties": { + }, + "isReadonly": false, + "isSystem": false, + "isLatest": true, + "isPublished": false, + "options": { + "autoUpdateConsumingWorkflows": false + }, + "root": { + "id": "8163a86bbcff6777", + "nodeId": "Workflow1:8163a86bbcff6777", + "name": "Flowchart1", + "type": "Elsa.Flowchart", + "version": 1, + "customProperties": { + "notFoundConnections": [ + ], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + }, + "activities": [ + { + "path": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "test" + } + }, + "supportedMethods": { + "typeName": "List", + "expression": { + "type": "Literal", + "value": "[\"GET\"]" + } + }, + "authorize": { + "typeName": "Boolean", + "expression": { + "type": "Literal", + "value": false + } + }, + "policy": { + "typeName": "String", + "expression": { + "type": "Literal" + } + }, + "requestTimeout": null, + "requestSizeLimit": null, + "fileSizeLimit": null, + "allowedFileExtensions": null, + "blockedFileExtensions": null, + "allowedMimeTypes": null, + "exposeRequestTooLargeOutcome": false, + "exposeFileTooLargeOutcome": false, + "exposeInvalidFileExtensionOutcome": false, + "exposeInvalidFileMimeTypeOutcome": false, + "parsedContent": null, + "files": null, + "routeData": null, + "queryStringData": null, + "headers": null, + "result": null, + "id": "b036c71bbd31983d", + "nodeId": "Workflow1:8163a86bbcff6777:b036c71bbd31983d", + "name": "HttpEndpoint1", + "type": "Elsa.HttpEndpoint", + "version": 1, + "customProperties": { + "canStartWorkflow": true, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -180, + "y": -80 + }, + "size": { + "width": 173.5625, + "height": 49.600002288818359 + } + } + } + }, + { + "text": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "Hello from http" + } + }, + "id": "b91befb34b3e44ea", + "nodeId": "Workflow1:8163a86bbcff6777:b91befb34b3e44ea", + "name": "WriteLine1", + "type": "Elsa.WriteLine", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": 133.0999755859375, + "y": -80 + }, + "size": { + "width": 138.02500915527344, + "height": 49.600002288818359 + } + } + } + } + ], + "variables": [ + ], + "connections": [ + { + "source": { + "activity": "b036c71bbd31983d", + "port": "Done" + }, + "target": { + "activity": "b91befb34b3e44ea", + "port": "In" + } + } + ] + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/timer-workflow.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/timer-workflow.json new file mode 100644 index 000000000..2af0eff90 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ImportAndPublish/Workflows/timer-workflow.json @@ -0,0 +1,117 @@ +{ + "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json", + "id": "6e217c3e1fbce6e", + "definitionId": "d1d828033d9c2dcd", + "name": "TimerWorkflow", + "createdAt": "2025-05-12T12:28:56.845918+00:00", + "version": 1, + "toolVersion": "3.3.0.0", + "variables": [ + ], + "inputs": [ + ], + "outputs": [ + ], + "outcomes": [ + ], + "customProperties": { + }, + "isReadonly": false, + "isSystem": false, + "isLatest": true, + "isPublished": false, + "options": { + "autoUpdateConsumingWorkflows": false + }, + "root": { + "id": "8163a86bbcff6777", + "nodeId": "Workflow1:8163a86bbcff6777", + "name": "Flowchart1", + "type": "Elsa.Flowchart", + "version": 1, + "customProperties": { + "notFoundConnections": [ + ], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + }, + "activities": [ + { + "text": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "Hello from timer" + } + }, + "id": "b91befb34b3e44ea", + "nodeId": "Workflow1:8163a86bbcff6777:b91befb34b3e44ea", + "name": "WriteLine1", + "type": "Elsa.WriteLine", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": 133.0999755859375, + "y": -80 + }, + "size": { + "width": 138.02500915527344, + "height": 49.600002288818359 + } + } + } + }, + { + "interval": { + "typeName": "TimeSpan", + "expression": { + "type": "Literal", + "value": "00:01:00" + } + }, + "id": "89ab56281a99239", + "nodeId": "Workflow1:8163a86bbcff6777:89ab56281a99239", + "name": "Timer1", + "type": "Elsa.Timer", + "version": 1, + "customProperties": { + "canStartWorkflow": true, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -81.899993896484375, + "y": -80 + }, + "size": { + "width": 107.63750457763672, + "height": 49.600002288818359 + } + } + } + } + ], + "variables": [ + ], + "connections": [ + { + "source": { + "activity": "89ab56281a99239", + "port": "Done" + }, + "target": { + "activity": "b91befb34b3e44ea", + "port": "In" + } + } + ] + } +} \ No newline at end of file