From 45f27f91f5bdadf6609a2de7add16ef217c33a8b Mon Sep 17 00:00:00 2001 From: MariusVuscanNx <96233009+MariusVuscanNx@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:07:04 +0300 Subject: [PATCH] Import publish execute integration test (#3872) * Import publish execute integration test * Update test * Extracted import logic into WorkflowDefinitionImporter * Removed the WorkflowDefinitionRequest model and done a model rename --------- Co-authored-by: Sipke Schoorstra --- .../Elsa.Workflows.Api/AssemblyInfo.cs | 3 + .../WorkflowDefinitions/Import/Endpoint.cs | 58 ++----------- .../WorkflowDefinitions/Post/Endpoint.cs | 4 +- .../Models/WorkflowDefinitionRequest.cs | 21 ----- .../Contracts/IWorkflowDefinitionImporter.cs | 10 +++ .../Features/WorkflowManagementFeature.cs | 5 +- .../Models/SaveWorkflowDefinitionRequest.cs | 21 +++++ .../Services/WorkflowDefinitionImporter.cs | 70 ++++++++++++++++ .../Elsa.IntegrationTests.csproj | 7 ++ .../Scenarios/ImportAndExecute/Tests.cs | 82 +++++++++++++++++++ .../Scenarios/ImportAndExecute/workflow.json | 55 +++++++++++++ 11 files changed, 261 insertions(+), 75 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Api/AssemblyInfo.cs delete mode 100644 src/modules/Elsa.Workflows.Api/Models/WorkflowDefinitionRequest.cs create mode 100644 src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionImporter.cs create mode 100644 src/modules/Elsa.Workflows.Management/Models/SaveWorkflowDefinitionRequest.cs create mode 100644 src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/Tests.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/workflow.json diff --git a/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs b/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs new file mode 100644 index 000000000..a16571941 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Elsa.IntegrationTests")] \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs index b55ef7339..645e365c7 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Import/Endpoint.cs @@ -1,11 +1,6 @@ -using System.Text.Json; using Elsa.Abstractions; -using Elsa.Common.Models; using Elsa.Workflows.Api.Models; -using Elsa.Workflows.Core.Serialization; using Elsa.Workflows.Management.Contracts; -using Elsa.Workflows.Management.Mappers; -using Elsa.Workflows.Management.Materializers; using Elsa.Workflows.Management.Models; using Elsa.Workflows.Runtime.Contracts; @@ -14,24 +9,18 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import; /// /// Imports a JSON file containing a workflow definition. /// -internal class Import : ElsaEndpoint +internal class Import : ElsaEndpoint { - private readonly SerializerOptionsProvider _serializerOptionsProvider; - private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher; private readonly IWorkflowDefinitionService _workflowDefinitionService; - private readonly VariableDefinitionMapper _variableDefinitionMapper; + private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter; /// public Import( - SerializerOptionsProvider serializerOptionsProvider, - IWorkflowDefinitionPublisher workflowDefinitionPublisher, IWorkflowDefinitionService workflowDefinitionService, - VariableDefinitionMapper variableDefinitionMapper) + IWorkflowDefinitionImporter workflowDefinitionImporter) { - _serializerOptionsProvider = serializerOptionsProvider; - _workflowDefinitionPublisher = workflowDefinitionPublisher; _workflowDefinitionService = workflowDefinitionService; - _variableDefinitionMapper = variableDefinitionMapper; + _workflowDefinitionImporter = workflowDefinitionImporter; } /// @@ -43,44 +32,13 @@ internal class Import : ElsaEndpoint - public override async Task HandleAsync(WorkflowDefinitionRequest request, CancellationToken cancellationToken) + public override async Task HandleAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken) { var definitionId = request.DefinitionId; + var isNew = string.IsNullOrWhiteSpace(definitionId); - // Get a workflow draft version. - var draft = !string.IsNullOrWhiteSpace(definitionId) - ? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken) - : default; - - var isNew = draft == null; - - // Create a new workflow in case no existing definition was found. - if (isNew) - { - draft = _workflowDefinitionPublisher.New(); - - if (!string.IsNullOrWhiteSpace(definitionId)) - draft.DefinitionId = definitionId; - } - - // Update the draft with the received model. - var root = request.Root; - var serializerOptions = _serializerOptionsProvider.CreateApiOptions(); - var stringData = JsonSerializer.Serialize(root, serializerOptions); - var variables = _variableDefinitionMapper.Map(request.Variables).ToList(); - - draft!.StringData = stringData; - draft.MaterializerName = JsonWorkflowMaterializer.MaterializerName; - draft.Name = request.Name?.Trim(); - draft.Description = request.Description?.Trim(); - draft.CustomProperties = request.CustomProperties ?? new Dictionary(); - draft.Variables = variables; - draft.Inputs = request.Inputs ?? new List(); - draft.Outputs = request.Outputs ?? new List(); - draft.Outcomes = request.Outcomes ?? new List(); - draft.Options = request.Options; - draft.UsableAsActivity = request.UsableAsActivity; - draft = request.Publish ? await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken) : await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken); + // Import workflow + var draft = await _workflowDefinitionImporter.ImportAsync(request, cancellationToken); // Materialize the workflow definition for serialization. var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(draft, cancellationToken); diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs index 2a7419087..cf7627fd0 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Post/Endpoint.cs @@ -17,7 +17,7 @@ using Microsoft.AspNetCore.Http; namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post; [PublicAPI] -internal class Post : ElsaEndpoint +internal class Post : ElsaEndpoint { private readonly SerializerOptionsProvider _serializerOptionsProvider; private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher; @@ -42,7 +42,7 @@ internal class Post : ElsaEndpoint? Variables { get; set; } - public ICollection? Inputs { get; set; } - public ICollection? Outputs { get; set; } - public ICollection? Outcomes { get; set; } - public IDictionary? CustomProperties { get; set; } - public IActivity? Root { get; set; } - public WorkflowOptions? Options { get; set; } - public bool Publish { get; set; } - public bool? UsableAsActivity { get; set; } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionImporter.cs b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionImporter.cs new file mode 100644 index 000000000..638ab309f --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Contracts/IWorkflowDefinitionImporter.cs @@ -0,0 +1,10 @@ +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Models; + +namespace Elsa.Workflows.Management.Contracts +{ + public interface IWorkflowDefinitionImporter + { + Task ImportAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken = default); + } +} diff --git a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs index 1e8fefb42..51e7fc869 100644 --- a/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs +++ b/src/modules/Elsa.Workflows.Management/Features/WorkflowManagementFeature.cs @@ -103,7 +103,7 @@ public class WorkflowManagementFeature : FeatureBase /// Adds the specified variable type to the system. /// public WorkflowManagementFeature AddVariableType(string category) => AddVariableType(typeof(T), category); - + /// /// Adds the specified variable type to the system. /// @@ -138,6 +138,7 @@ public class WorkflowManagementFeature : FeatureBase .AddMemoryStore() .AddActivityProvider() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -151,7 +152,7 @@ public class WorkflowManagementFeature : FeatureBase .AddSingleton() .AddSingleton() ; - + Services.AddNotificationHandlersFrom(GetType()); Services.Configure(options => diff --git a/src/modules/Elsa.Workflows.Management/Models/SaveWorkflowDefinitionRequest.cs b/src/modules/Elsa.Workflows.Management/Models/SaveWorkflowDefinitionRequest.cs new file mode 100644 index 000000000..35b834990 --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Models/SaveWorkflowDefinitionRequest.cs @@ -0,0 +1,21 @@ +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Models; + +namespace Elsa.Workflows.Management.Models +{ + public class SaveWorkflowDefinitionRequest + { + public string? DefinitionId { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public ICollection? Variables { get; set; } + public ICollection? Inputs { get; set; } + public ICollection? Outputs { get; set; } + public ICollection? Outcomes { get; set; } + public IDictionary? CustomProperties { get; set; } + public IActivity? Root { get; set; } + public WorkflowOptions? Options { get; set; } + public bool Publish { get; set; } + public bool? UsableAsActivity { get; set; } + } +} diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs new file mode 100644 index 000000000..259221a63 --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionImporter.cs @@ -0,0 +1,70 @@ +using Elsa.Common.Models; +using Elsa.Workflows.Core.Serialization; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Mappers; +using Elsa.Workflows.Management.Materializers; +using Elsa.Workflows.Management.Models; +using System.Text.Json; + +namespace Elsa.Workflows.Management.Services +{ + public class WorkflowDefinitionImporter : IWorkflowDefinitionImporter + { + private readonly SerializerOptionsProvider _serializerOptionsProvider; + private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher; + private readonly VariableDefinitionMapper _variableDefinitionMapper; + + public WorkflowDefinitionImporter( + SerializerOptionsProvider serializerOptionsProvider, + IWorkflowDefinitionPublisher workflowDefinitionPublisher, + VariableDefinitionMapper variableDefinitionMapper) + { + _serializerOptionsProvider = serializerOptionsProvider; + _workflowDefinitionPublisher = workflowDefinitionPublisher; + _variableDefinitionMapper = variableDefinitionMapper; + } + + public async Task ImportAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken = default) + { + var definitionId = request.DefinitionId; + + // Get a workflow draft version. + var draft = !string.IsNullOrWhiteSpace(definitionId) + ? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken) + : default; + + var isNew = draft == null; + + // Create a new workflow in case no existing definition was found. + if (isNew) + { + draft = _workflowDefinitionPublisher.New(); + + if (!string.IsNullOrWhiteSpace(definitionId)) + draft.DefinitionId = definitionId; + } + + // Update the draft with the received model. + var root = request.Root; + var serializerOptions = _serializerOptionsProvider.CreateApiOptions(); + var stringData = JsonSerializer.Serialize(root, serializerOptions); + var variables = _variableDefinitionMapper.Map(request.Variables).ToList(); + + draft!.StringData = stringData; + draft.MaterializerName = JsonWorkflowMaterializer.MaterializerName; + draft.Name = request.Name?.Trim(); + draft.Description = request.Description?.Trim(); + draft.CustomProperties = request.CustomProperties ?? new Dictionary(); + draft.Variables = variables; + draft.Inputs = request.Inputs ?? new List(); + draft.Outputs = request.Outputs ?? new List(); + draft.Outcomes = request.Outcomes ?? new List(); + draft.Options = request.Options; + draft.UsableAsActivity = request.UsableAsActivity; + draft = request.Publish ? await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken) : await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken); + + return draft; + } + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj index 7eed97924..f255dbd3f 100644 --- a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj +++ b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj @@ -23,7 +23,14 @@ + + + + PreserveNewest + + + diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/Tests.cs new file mode 100644 index 000000000..b6ab1d50c --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/Tests.cs @@ -0,0 +1,82 @@ +using Elsa.Common.Models; +using Elsa.Expressions.Contracts; +using Elsa.Extensions; +using Elsa.Testing.Shared; +using Elsa.Workflows.Api.Models; +using Elsa.Workflows.Core.Activities; +using Elsa.Workflows.Core.Activities.Flowchart.Activities; +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Models; +using Elsa.Workflows.Core.Serialization; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Models; +using Elsa.Workflows.Runtime.Contracts; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.IntegrationTests.Scenarios.ImportAndExecute; + +public class Tests +{ + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter; + private readonly SerializerOptionsProvider _serializerOptionsProvider; + private readonly IWorkflowRuntime _workflowRuntime; + private readonly IActivityRegistry _activityRegistry; + private readonly IExpressionSyntaxRegistry _expressionSyntaxRegistry; + private readonly IEnumerable _expressionSyntaxProviders; + + public Tests(ITestOutputHelper testOutputHelper) + { + var services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .ConfigureElsa(elsa => elsa.UseWorkflowsApi()) + .Build(); + _serializerOptionsProvider = services.GetRequiredService(); + _workflowRuntime = services.GetRequiredService(); + _activityRegistry = services.GetRequiredService(); + _expressionSyntaxRegistry = services.GetRequiredService(); + _expressionSyntaxProviders = services.GetServices(); + _workflowDefinitionImporter = services.GetRequiredService(); + } + + [Fact(DisplayName = "Workflow imported from file should execute successfully.")] + public async Task Test1() + { + // Register activities. + await _activityRegistry.RegisterAsync(typeof(Workflow)); + await _activityRegistry.RegisterAsync(typeof(Flowchart)); + await _activityRegistry.RegisterAsync(typeof(WriteLine)); + + // Register expression syntaxes. + foreach (var syntaxProvider in _expressionSyntaxProviders) + { + var syntaxes = await syntaxProvider.GetDescriptorsAsync(); + _expressionSyntaxRegistry.AddMany(syntaxes); + } + + // Import and publish workflow. + var fileName = @"Scenarios/ImportAndExecute/workflow.json"; + await using var openStream = File.OpenRead(fileName); + var options = _serializerOptionsProvider.CreateApiOptions(); + var workflowDefinitionRequest = (await JsonSerializer.DeserializeAsync(openStream, options))!; + + workflowDefinitionRequest.Publish = true; + var workflowDefinition = await _workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest); + + // Execute. + var startWorkflowOptions = new StartWorkflowRuntimeOptions(null, new Dictionary(), VersionOptions.Published); + await _workflowRuntime.StartWorkflowAsync(workflowDefinition.DefinitionId, startWorkflowOptions); + + // Assert. + var lines = _capturingTextWriter.Lines.ToList(); + + Assert.Equal(new[] { "Dummy Text" }, lines); + } +} diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/workflow.json b/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/workflow.json new file mode 100644 index 000000000..45c905c9c --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImportAndExecute/workflow.json @@ -0,0 +1,55 @@ +{ + "id": "3a793167cf6948c899d2f40c3c05cf0b", + "definitionId": "11e2b350dae8485498c16d1ce3843829", + "name": "Dummy workflow", + "createdAt": "2023-04-04T08:35:22.841043+00:00", + "version": 1, + "variables": [], + "inputs": [], + "outputs": [], + "outcomes": [], + "metadata": {}, + "isLatest": true, + "isPublished": true, + "root": { + "type": "Elsa.Flowchart", + "version": 1, + "id": "Flowchart1", + "metadata": {}, + "applicationProperties": { + "NotFoundConnectionsKey": [] + }, + "start": "WriteLine1", + "activities": [ + { + "text": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "Dummy Text" + }, + "memoryReference": { + "id": "WriteLine1:input-1" + } + }, + "id": "WriteLine1", + "type": "Elsa.WriteLine", + "version": 1, + "canStartWorkflow": false, + "runAsynchronously": false, + "customProperties": {}, + "source": null, + "line": null, + "metadata": { + "designer": { + "position": { + "x": 2256, + "y": 2462 + } + } + } + } + ], + "connections": [] + } +} \ No newline at end of file