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 <sipkeschoorstra@outlook.com>
This commit is contained in:
MariusVuscanNx 2023-04-05 20:07:04 +03:00 committed by GitHub
parent ef1ec1ed08
commit 45f27f91f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 261 additions and 75 deletions

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Elsa.IntegrationTests")]

View file

@ -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;
/// <summary>
/// Imports a JSON file containing a workflow definition.
/// </summary>
internal class Import : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinitionResponse>
internal class Import : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefinitionResponse>
{
private readonly SerializerOptionsProvider _serializerOptionsProvider;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly VariableDefinitionMapper _variableDefinitionMapper;
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
/// <inheritdoc />
public Import(
SerializerOptionsProvider serializerOptionsProvider,
IWorkflowDefinitionPublisher workflowDefinitionPublisher,
IWorkflowDefinitionService workflowDefinitionService,
VariableDefinitionMapper variableDefinitionMapper)
IWorkflowDefinitionImporter workflowDefinitionImporter)
{
_serializerOptionsProvider = serializerOptionsProvider;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
_workflowDefinitionService = workflowDefinitionService;
_variableDefinitionMapper = variableDefinitionMapper;
_workflowDefinitionImporter = workflowDefinitionImporter;
}
/// <inheritdoc />
@ -43,44 +32,13 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefiniti
}
/// <inheritdoc />
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<string, object>();
draft.Variables = variables;
draft.Inputs = request.Inputs ?? new List<InputDefinition>();
draft.Outputs = request.Outputs ?? new List<OutputDefinition>();
draft.Outcomes = request.Outcomes ?? new List<string>();
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);

View file

@ -17,7 +17,7 @@ using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post;
[PublicAPI]
internal class Post : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinitionResponse, WorkflowDefinitionMapper>
internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefinitionResponse, WorkflowDefinitionMapper>
{
private readonly SerializerOptionsProvider _serializerOptionsProvider;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
@ -42,7 +42,7 @@ internal class Post : ElsaEndpoint<WorkflowDefinitionRequest, WorkflowDefinition
ConfigurePermissions("write:workflow-definitions");
}
public override async Task HandleAsync(WorkflowDefinitionRequest request, CancellationToken cancellationToken)
public override async Task HandleAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken)
{
var definitionId = request.DefinitionId;
var resourceName = $"{GetType().FullName}:{(!string.IsNullOrWhiteSpace(definitionId) ? definitionId : Guid.NewGuid().ToString())}";

View file

@ -1,21 +0,0 @@
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Api.Models;
internal class WorkflowDefinitionRequest
{
public string? DefinitionId { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public ICollection<VariableDefinition>? Variables { get; set; }
public ICollection<InputDefinition>? Inputs { get; set; }
public ICollection<OutputDefinition>? Outputs { get; set; }
public ICollection<string>? Outcomes { get; set; }
public IDictionary<string, object>? CustomProperties { get; set; }
public IActivity? Root { get; set; }
public WorkflowOptions? Options { get; set; }
public bool Publish { get; set; }
public bool? UsableAsActivity { get; set; }
}

View file

@ -0,0 +1,10 @@
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Management.Contracts
{
public interface IWorkflowDefinitionImporter
{
Task<WorkflowDefinition> ImportAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken = default);
}
}

View file

@ -103,7 +103,7 @@ public class WorkflowManagementFeature : FeatureBase
/// Adds the specified variable type to the system.
/// </summary>
public WorkflowManagementFeature AddVariableType<T>(string category) => AddVariableType(typeof(T), category);
/// <summary>
/// Adds the specified variable type to the system.
/// </summary>
@ -138,6 +138,7 @@ public class WorkflowManagementFeature : FeatureBase
.AddMemoryStore<WorkflowInstance, MemoryWorkflowInstanceStore>()
.AddActivityProvider<TypedActivityProvider>()
.AddSingleton<IWorkflowDefinitionPublisher, WorkflowDefinitionPublisher>()
.AddSingleton<IWorkflowDefinitionImporter, WorkflowDefinitionImporter>()
.AddSingleton<IWorkflowDefinitionManager, WorkflowDefinitionManager>()
.AddSingleton<IActivityRegistryPopulator, ActivityRegistryPopulator>()
.AddSingleton<IExpressionSyntaxRegistry, ExpressionSyntaxRegistry>()
@ -151,7 +152,7 @@ public class WorkflowManagementFeature : FeatureBase
.AddSingleton<SerializerOptionsProvider>()
.AddSingleton<VariableDefinitionMapper>()
;
Services.AddNotificationHandlersFrom(GetType());
Services.Configure<ManagementOptions>(options =>

View file

@ -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<VariableDefinition>? Variables { get; set; }
public ICollection<InputDefinition>? Inputs { get; set; }
public ICollection<OutputDefinition>? Outputs { get; set; }
public ICollection<string>? Outcomes { get; set; }
public IDictionary<string, object>? CustomProperties { get; set; }
public IActivity? Root { get; set; }
public WorkflowOptions? Options { get; set; }
public bool Publish { get; set; }
public bool? UsableAsActivity { get; set; }
}
}

View file

@ -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<WorkflowDefinition> 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<string, object>();
draft.Variables = variables;
draft.Inputs = request.Inputs ?? new List<InputDefinition>();
draft.Outputs = request.Outputs ?? new List<OutputDefinition>();
draft.Outcomes = request.Outcomes ?? new List<string>();
draft.Options = request.Options;
draft.UsableAsActivity = request.UsableAsActivity;
draft = request.Publish ? await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken) : await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken);
return draft;
}
}
}

View file

@ -23,7 +23,14 @@
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.JavaScript\Elsa.JavaScript.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Scenarios\ImportAndExecute\workflow.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -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<IExpressionSyntaxProvider> _expressionSyntaxProviders;
public Tests(ITestOutputHelper testOutputHelper)
{
var services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.ConfigureElsa(elsa => elsa.UseWorkflowsApi())
.Build();
_serializerOptionsProvider = services.GetRequiredService<SerializerOptionsProvider>();
_workflowRuntime = services.GetRequiredService<IWorkflowRuntime>();
_activityRegistry = services.GetRequiredService<IActivityRegistry>();
_expressionSyntaxRegistry = services.GetRequiredService<IExpressionSyntaxRegistry>();
_expressionSyntaxProviders = services.GetServices<IExpressionSyntaxProvider>();
_workflowDefinitionImporter = services.GetRequiredService<IWorkflowDefinitionImporter>();
}
[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<SaveWorkflowDefinitionRequest>(openStream, options))!;
workflowDefinitionRequest.Publish = true;
var workflowDefinition = await _workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
// Execute.
var startWorkflowOptions = new StartWorkflowRuntimeOptions(null, new Dictionary<string, object>(), VersionOptions.Published);
await _workflowRuntime.StartWorkflowAsync(workflowDefinition.DefinitionId, startWorkflowOptions);
// Assert.
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Dummy Text" }, lines);
}
}

View file

@ -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": []
}
}