Add project files.

This commit is contained in:
2025-08-18 17:11:43 +03:30
parent 807dc263be
commit e5eb10d604
3943 changed files with 223719 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="coverlet.collector" PrivateAssets="all" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
</ItemGroup>
</Project>
+10
View File
@@ -0,0 +1,10 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<ItemGroup>
<PackageReference Include="Testcontainers.PostgreSql"/>
<PackageReference Include="Testcontainers.RabbitMq"/>
</ItemGroup>
</Project>
@@ -0,0 +1,92 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\apps\Elsa.Server.Web\Elsa.Server.Web.csproj"/>
<ProjectReference Include="..\..\..\src\clients\Elsa.Api.Client\Elsa.Api.Client.csproj"/>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Component\Elsa.Testing.Shared.Component.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Caching.Distributed.MassTransit\Elsa.Caching.Distributed.MassTransit.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\BasicWorkflows\hello-world.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowCompletion\fork.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\LogPersistenceModes\input-output-logging-1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\LogPersistenceModes\input-output-logging-2.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\LogPersistenceModes\input-output-logging-3.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\LogPersistenceModes\input-output-logging-3-child.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowCompletion\hello-world.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\CachingAndWorkflowDefinitionActivity\Workflows\workflow-definition-child.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\CachingAndWorkflowDefinitionActivity\Workflows\workflow-definition-grand-child.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\CachingAndWorkflowDefinitionActivity\Workflows\workflow-definition-parent.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowActivities\Workflows\CacheInvalidation_Child.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowActivities\Workflows\CacheInvalidation_Parent.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowActivities\Workflows\CacheInvalidation_Childv2.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowJsonStructures\main.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowJsonStructures\sub.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionRefresh\dynamic-endpoint-http-workflow.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionReload\http-workflow.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\WorkflowDefinitionReload\Workflows\http-workflow.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\HttpWorkflows\Workflows\http-hello-world.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\InputOutput\Workflows\consumer.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\InputOutput\Workflows\provider.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers_005Ccontracts/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helpers_005Ceventargs/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>
@@ -0,0 +1,25 @@
using Elsa.Workflows.ComponentTests.Fixtures;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Abstractions;
[Collection(nameof(AppCollection))]
public abstract class AppComponentTest(App app) : IDisposable
{
protected WorkflowServer WorkflowServer { get; } = app.WorkflowServer;
protected Cluster Cluster { get; } = app.Cluster;
protected Infrastructure Infrastructure { get; } = app.Infrastructure;
protected IServiceScope Scope { get; } = app.WorkflowServer.Services.CreateScope();
void IDisposable.Dispose()
{
// Disposing the Scope here and in other places where it is created somehow seems to cause the test runner to hang when running other test projects.
// Let's comment it out for the time being.
//Scope.Dispose();
OnDispose();
}
protected virtual void OnDispose()
{
}
}
@@ -0,0 +1,56 @@
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Testing.Shared.Services;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
namespace Elsa.Workflows.ComponentTests.Activities;
public class TriggerSignal : CodeActivity
{
/// <inheritdoc />
[JsonConstructor]
private TriggerSignal(string? source = null, int? line = null) : base(source, line)
{
}
/// <inheritdoc />
public TriggerSignal(string eventName, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(new Literal<string>(eventName), source, line)
{
}
/// <inheritdoc />
public TriggerSignal(Func<string> eventName, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null)
: this(Expression.DelegateExpression(eventName), source, line)
{
}
/// <inheritdoc />
public TriggerSignal(Func<ExpressionExecutionContext, string?> eventName, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null)
: this(Expression.DelegateExpression(eventName), source, line)
{
}
/// <inheritdoc />
public TriggerSignal(Variable<string> variable, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line) => EventName = new Input<string>(variable);
/// <inheritdoc />
public TriggerSignal(Literal<string> literal, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line) => EventName = new Input<string>(literal);
/// <inheritdoc />
public TriggerSignal(Expression expression, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line) => EventName = new Input<string>(expression, new MemoryBlockReference());
/// <inheritdoc />
public TriggerSignal(Input<string> eventName, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line) => EventName = eventName;
public Input<string> EventName { get; set; }
protected override void Execute(ActivityExecutionContext context)
{
var testEventManager = context.GetRequiredService<SignalManager>();
var eventName = EventName.Get(context);
testEventManager.Trigger(eventName);
}
}
@@ -0,0 +1,17 @@
using Elsa.MassTransit.Messages;
using Elsa.Testing.Shared;
using Elsa.Testing.Shared.Services;
using Hangfire.Annotations;
using MassTransit;
namespace Elsa.Workflows.ComponentTests.Consumers;
[UsedImplicitly]
public class WorkflowDefinitionEventConsumer(WorkflowDefinitionEvents workflowDefinitionEvents) : IConsumer<WorkflowDefinitionDeleted>
{
public Task Consume(ConsumeContext<WorkflowDefinitionDeleted> context)
{
workflowDefinitionEvents.OnWorkflowDefinitionDeleted(new WorkflowDefinitionDeletedEventArgs(context.Message.Id));
return Task.CompletedTask;
}
}
@@ -0,0 +1,26 @@
using Elsa.Caching;
using Elsa.Testing.Shared;
using Elsa.Testing.Shared.Services;
using Hangfire.Annotations;
using Microsoft.Extensions.Primitives;
namespace Elsa.Workflows.ComponentTests.Decorators;
/// <summary>
/// Provides a decorator for the `IChangeTokenSignaler` interface that triggers change token events.
/// This class is used to raise change token signal events and delegate the actual work to the decorated service.
/// </summary>
[UsedImplicitly]
public class EventPublishingChangeTokenSignaler(IChangeTokenSignaler decoratedService, TriggerChangeTokenSignalEvents triggerChangeTokenSignalEvents) : IChangeTokenSignaler
{
public IChangeToken GetToken(string key)
{
return decoratedService.GetToken(key);
}
public ValueTask TriggerTokenAsync(string key, CancellationToken cancellationToken = default)
{
triggerChangeTokenSignalEvents.RaiseChangeTokenSignalTriggered(new TriggerChangeTokenSignalEventArgs(key));
return decoratedService.TriggerTokenAsync(key, cancellationToken);
}
}
@@ -0,0 +1,29 @@
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Fixtures;
[UsedImplicitly]
public class App : IAsyncLifetime
{
public App()
{
Infrastructure = new();
Cluster = new(Infrastructure);
WorkflowServer = Cluster.Pod1;
}
public Infrastructure Infrastructure { get; set; }
public Cluster Cluster { get; set; }
public WorkflowServer WorkflowServer { get; set; }
public async Task InitializeAsync()
{
await Infrastructure.InitializeAsync();
}
public async Task DisposeAsync()
{
await Cluster.DisposeAsync();
await Infrastructure.DisposeAsync();
}
}
@@ -0,0 +1,9 @@
namespace Elsa.Workflows.ComponentTests.Fixtures;
[CollectionDefinition(nameof(AppCollection))]
public class AppCollection : ICollectionFixture<App>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}
@@ -0,0 +1,18 @@
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Fixtures;
[UsedImplicitly]
public class Cluster(Infrastructure infrastructure) : IAsyncDisposable
{
public WorkflowServer Pod3 { get; set; } = new(infrastructure, "http://localhost:5003");
public WorkflowServer Pod2 { get; set; } = new(infrastructure, "http://localhost:5002");
public WorkflowServer Pod1 { get; set; } = new(infrastructure, "http://localhost:5001");
public async ValueTask DisposeAsync()
{
await Pod3.DisposeAsync();
await Pod2.DisposeAsync();
await Pod1.DisposeAsync();
}
}
@@ -0,0 +1,32 @@
using Testcontainers.PostgreSql;
using Testcontainers.RabbitMq;
namespace Elsa.Workflows.ComponentTests.Fixtures;
public class Infrastructure : IAsyncLifetime
{
public readonly PostgreSqlContainer DbContainer = new PostgreSqlBuilder()
.WithImage("postgres:latest")
.WithDatabase("elsa")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
public readonly RabbitMqContainer RabbitMqContainer = new RabbitMqBuilder()
.WithImage("rabbitmq:4-management")
.Build();
public Task InitializeAsync()
{
return Task.WhenAll(
DbContainer.StartAsync(),
RabbitMqContainer.StartAsync());
}
public Task DisposeAsync()
{
return Task.WhenAll(
DbContainer.StopAsync(),
RabbitMqContainer.StopAsync());
}
}
@@ -0,0 +1,138 @@
using System.Reflection;
using Elsa.Alterations.Extensions;
using Elsa.Caching;
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Alterations;
using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Identity.Providers;
using Elsa.MassTransit.Extensions;
using Elsa.Testing.Shared.Handlers;
using Elsa.Testing.Shared.Services;
using Elsa.Workflows.ComponentTests.Consumers;
using Elsa.Workflows.ComponentTests.Decorators;
using Elsa.Workflows.ComponentTests.Materializers;
using Elsa.Workflows.ComponentTests.WorkflowProviders;
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime.Distributed.Extensions;
using FluentStorage;
using Hangfire.Annotations;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Refit;
using static Elsa.Api.Client.RefitSettingsHelper;
namespace Elsa.Workflows.ComponentTests.Fixtures;
[UsedImplicitly]
public class WorkflowServer(Infrastructure infrastructure, string url) : WebApplicationFactory<Program>
{
public TClient CreateApiClient<TClient>()
{
var client = CreateClient();
client.BaseAddress = new(client.BaseAddress!, "/elsa/api");
client.Timeout = TimeSpan.FromMinutes(1);
return RestService.For<TClient>(client, CreateRefitSettings(Services));
}
public HttpClient CreateHttpWorkflowClient()
{
var client = CreateClient();
client.BaseAddress = new(client.BaseAddress!, "/workflows/");
client.Timeout = TimeSpan.FromMinutes(1);
return client;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
var dbConnectionString = infrastructure.DbContainer.GetConnectionString();
var rabbitMqConnectionString = infrastructure.RabbitMqContainer.GetConnectionString();
builder.UseUrls(url);
if (Program.ConfigureForTest == null)
{
Program.ConfigureForTest = elsa =>
{
elsa.AddWorkflowsFrom<WorkflowServer>();
elsa.AddActivitiesFrom<WorkflowServer>();
elsa.UseDefaultAuthentication(defaultAuthentication => defaultAuthentication.UseAdminApiKey());
elsa.UseFluentStorageProvider(sp =>
{
var assemblyLocation = Assembly.GetExecutingAssembly().Location;
var assemblyDirectory = Path.GetDirectoryName(assemblyLocation)!;
var workflowsDirectorySegments = new[]
{
assemblyDirectory, "Scenarios"
};
var workflowsDirectory = Path.Join(workflowsDirectorySegments);
return StorageFactory.Blobs.DirectoryFiles(workflowsDirectory);
});
elsa.UseMassTransit(massTransit =>
{
//massTransit.UseRabbitMq(rabbitMqConnectionString);
massTransit.Services.AddSingleton<WorkflowDefinitionEvents>();
massTransit.AddConsumer<WorkflowDefinitionEventConsumer>("elsa-test-workflow-definition-updates", true);
});
elsa.UseIdentity(identity => identity.UseEntityFrameworkCore(ef => ef.UsePostgreSql(dbConnectionString)));
elsa.UseWorkflowManagement(management =>
{
management.UseEntityFrameworkCore(ef => ef.UsePostgreSql(dbConnectionString));
management.UseMassTransitDispatcher();
management.UseCache();
});
elsa.UseWorkflowRuntime(runtime =>
{
runtime.UseEntityFrameworkCore(ef => ef.UsePostgreSql(dbConnectionString));
runtime.UseCache();
runtime.UseMassTransitDispatcher();
runtime.UseDistributedRuntime();
});
elsa.UseDistributedCache(distributedCaching =>
{
distributedCaching.UseMassTransit();
});
elsa.UseJavaScript(options =>
{
options.AllowClrAccess = true;
options.ConfigureEngine(engine =>
{
engine.SetValue("getStaticValue", () => StaticValueHolder.Value);
});
});
elsa.UseAlterations(alterations =>
{
alterations.UseEntityFrameworkCore(ef => ef.UsePostgreSql(dbConnectionString));
});
elsa.UseHttp(http =>
{
http.UseCache();
});
};
}
builder.ConfigureTestServices(services =>
{
services
.AddSingleton<SignalManager>()
.AddScoped<WorkflowEvents>()
.AddScoped<WorkflowDefinitionEvents>()
.AddSingleton<TriggerChangeTokenSignalEvents>()
.AddScoped<IWorkflowMaterializer, TestWorkflowMaterializer>()
.AddNotificationHandlersFrom<WorkflowServer>()
.AddWorkflowDefinitionProvider<TestWorkflowProvider>()
.AddNotificationHandlersFrom<WorkflowEventHandlers>()
.Decorate<IChangeTokenSignaler, EventPublishingChangeTokenSignaler>()
;
});
}
protected override void ConfigureClient(HttpClient client)
{
client.DefaultRequestHeaders.Authorization = new("ApiKey", AdminApiKeyProvider.DefaultApiKey);
}
}
@@ -0,0 +1,30 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.ComponentTests.WorkflowProviders;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime;
namespace Elsa.Workflows.ComponentTests.Materializers;
/// <summary>
/// A workflow materializer that deserializes workflows created from <see cref="TestWorkflowProvider"/>.
/// </summary>
public class TestWorkflowMaterializer(IEnumerable<IWorkflowsProvider> workflowProviders) : IWorkflowMaterializer
{
/// <summary>
/// The name of the materializer.
/// </summary>
public const string MaterializerName = "Test";
/// <inheritdoc />
public string Name => MaterializerName;
/// <inheritdoc />
public ValueTask<Workflow> MaterializeAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
{
var testProvider = (TestWorkflowProvider)workflowProviders.Single(x => x is TestWorkflowProvider);
var materializedWorkflow = testProvider.MaterializedWorkflows.First(x => x.Workflow.Identity.Id == definition.Id);
return ValueTask.FromResult(materializedWorkflow.Workflow);
}
}
@@ -0,0 +1,14 @@
using Elsa.Workflows.Runtime;
namespace Elsa.Workflows.ComponentTests.WorkflowProviders;
public class TestWorkflowProvider : IWorkflowsProvider
{
public ICollection<MaterializedWorkflow> MaterializedWorkflows { get; set; } = new List<MaterializedWorkflow>();
public string Name => "Test";
public ValueTask<IEnumerable<MaterializedWorkflow>> GetWorkflowsAsync(CancellationToken cancellationToken = default)
{
return new(MaterializedWorkflows);
}
}
@@ -0,0 +1,17 @@
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.FlowJoins;
public class Tests(App app) : AppComponentTest(app)
{
// https://github.com/elsa-workflows/elsa-core/issues/5348
[Fact]
public async Task FlowchartWithSingleFlowJoin_ShouldExecuteSuccessfully()
{
var workflowRunner = Scope.ServiceProvider.GetRequiredService<IWorkflowRunner>();
var result = await workflowRunner.RunAsync<SingleJoinWorkflow>();
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus);
}
}
@@ -0,0 +1,17 @@
using Elsa.Workflows.Activities.Flowchart.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.Activities.FlowJoins;
public class SingleJoinWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new Elsa.Workflows.Activities.Flowchart.Activities.Flowchart
{
Activities =
{
new FlowJoin()
}
};
}
}
@@ -0,0 +1,21 @@
using System.Net;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Testing.Shared.Extensions;
using Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Execute;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
namespace Elsa.Workflows.ComponentTests.Scenarios.BasicWorkflows;
public class HelloWorldTests(App app) : AppComponentTest(app)
{
[Fact]
public async Task HelloWorldWorkflow_ShouldReturnOk()
{
var client = WorkflowServer.CreateApiClient<IExecuteWorkflowApi>();
using var response = await client.ExecuteAsync("1590068018aa4f0a");
var model = await response.ReadAsJsonAsync<Response>(WorkflowServer.Services);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(WorkflowSubStatus.Finished, model.WorkflowState.SubStatus);
}
}
@@ -0,0 +1,42 @@
{
"id": "1590068018aa4f0a",
"definitionId": "1590068018aa4f0a",
"name": "Hello World",
"isLatest": true,
"isPublished": true,
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "969b0703a9379c3b",
"nodeId": "Workflow1:969b0703a9379c3b",
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello World!"
}
},
"id": "b039045bb7443e57",
"nodeId": "Workflow1:969b0703a9379c3b:b039045bb7443e57",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"metadata": {
"designer": {
"position": {
"x": -231.796875,
"y": 269
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,56 @@
using Elsa.Common.Models;
using Elsa.Testing.Shared.Services;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows;
public class BulkDispatchWorkflowsTests : AppComponentTest
{
private readonly SignalManager _signalManager;
private readonly IWorkflowRuntime _workflowRuntime;
public BulkDispatchWorkflowsTests(App app) : base(app)
{
_workflowRuntime = Scope.ServiceProvider.GetRequiredService<IWorkflowRuntime>();
_signalManager = Scope.ServiceProvider.GetRequiredService<SignalManager>();
}
// /// <summary>
// /// Dispatches and waits for child workflows to complete.
// /// </summary>
// [Fact(Skip = "This test is flaky and needs to be fixed.")]
// public async Task DispatchAndWaitWorkflow_ShouldWaitForChildWorkflowToComplete()
// {
// var workflowClient = await _workflowRuntime.CreateClientAsync();
// await workflowClient.CreateInstanceAsync(new CreateWorkflowInstanceRequest
// {
// WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(GreetEmployeesWorkflow.DefinitionId, VersionOptions.Published)
// });
// await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
// await _signalManager.WaitAsync<string>("Completed");
// }
/// <summary>
/// Individual items are sent as input to child workflows.
/// </summary>
[Fact]
public async Task DispatchWorkflows_ChildWorkflowsShouldReceiveCurrentItem()
{
var workflowClient = await _workflowRuntime.CreateClientAsync();
var request = new CreateAndRunWorkflowInstanceRequest
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(MixFruitsWorkflow.DefinitionId, VersionOptions.Published)
};
await workflowClient.CreateAndRunInstanceAsync(request);
await _signalManager.WaitAsync("Apple");
await _signalManager.WaitAsync("Banana");
await _signalManager.WaitAsync("Cherry");
}
}
@@ -0,0 +1,24 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows;
[UsedImplicitly]
public class EmployeeGreetingWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var employeeName = builder.WithInput<string>("Employee");
builder.Root = new Sequence
{
Activities =
{
new WriteLine(x => $"Hello {x.GetInput<string>(employeeName)}")
}
};
}
}
@@ -0,0 +1,26 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.ComponentTests.Activities;
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows;
[UsedImplicitly]
public class FruitWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var item = builder.WithInput<string>("Item");
builder.Root = new Sequence
{
Activities =
{
new WriteLine(x => $"Mixing {x.GetInput<string>(item)}"),
new TriggerSignal(x => x.GetInput<string>(item))
}
};
}
}
@@ -0,0 +1,37 @@
using Elsa.Testing.Shared.Activities;
using Elsa.Workflows.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows;
public class GreetEmployeesWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
var employeeNames = new[]
{
"Alice", "Bob", "Charlie"
};
var inputEntries = employeeNames.Select(x => new Dictionary<string, object>
{
["Employee"] = x
});
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
new Runtime.Activities.BulkDispatchWorkflows
{
WorkflowDefinitionId = new(EmployeeGreetingWorkflow.DefinitionId),
Items = new(inputEntries),
WaitForCompletion = new(true)
},
new TriggerSignal("Completed")
}
};
}
}
@@ -0,0 +1,30 @@
using Elsa.Workflows.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.BulkDispatchWorkflows.Workflows;
public class MixFruitsWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
var fruits = new[]
{
"Apple", "Banana", "Cherry"
};
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
new Runtime.Activities.BulkDispatchWorkflows
{
WorkflowDefinitionId = new(FruitWorkflow.DefinitionId),
Items = new(fruits),
WaitForCompletion = new(true)
}
}
};
}
}
@@ -0,0 +1,55 @@
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.ComponentTests.Scenarios.CachingAndWorkflowDefinitionActivity;
// Tests the behavior of the WorkflowDefinitionActivity.
// See https://github.com/elsa-workflows/elsa-core/issues/5314
public class WorkflowDefinitionActivityTests : AppComponentTest
{
private readonly ITestOutputHelper _testOutputHelper;
private const string GrandChildDefinitionId = "29595e7b37a4836d";
private readonly IWorkflowDefinitionCacheManager _workflowDefinitionCacheManager;
private readonly IWorkflowInstanceStore _workflowInstanceStore;
private readonly HttpClient _httpWorkflowClient;
public WorkflowDefinitionActivityTests(App app, ITestOutputHelper testOutputHelper) : base(app)
{
_testOutputHelper = testOutputHelper;
_httpWorkflowClient = WorkflowServer.CreateHttpWorkflowClient();
_workflowDefinitionCacheManager = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionCacheManager>();
_workflowInstanceStore = Scope.ServiceProvider.GetRequiredService<IWorkflowInstanceStore>();
}
[Fact]
public async Task SendHttpRequest_WhileEvictingCache_ShouldNotGenerateFaults()
{
var requestTasks = Enumerable.Range(0, 200).Select(SendRequestAsync).ToList();
await Task.WhenAll(requestTasks);
var filter = new WorkflowInstanceFilter
{
WorkflowSubStatus = WorkflowSubStatus.Faulted
};
var faultedWorkflows = (await _workflowInstanceStore.FindManyAsync(filter)).ToList();
var faultCount = faultedWorkflows.Count;
foreach (var faultedWorkflow in faultedWorkflows)
foreach (var incident in faultedWorkflow.WorkflowState.Incidents)
_testOutputHelper.WriteLine(incident.Message);
Assert.Equal(0, faultCount);
}
private async Task SendRequestAsync(int index = 0)
{
var requestTask = _httpWorkflowClient.PostAsync("parent", new StringContent("{}"));
var evictionTask = _workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(GrandChildDefinitionId);
await Task.WhenAll(requestTask, evictionTask);
}
}
@@ -0,0 +1,64 @@
{
"id": "26d186d68deb1250",
"definitionId": "a3390d1f4c2594a8",
"name": "Child",
"createdAt": "2024-04-30T08:22:44.543407+00:00",
"version": 1,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "e6c941b6f2292fcf",
"nodeId": "Workflow2:e6c941b6f2292fcf",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"workflowDefinitionId": "29595e7b37a4836d",
"workflowDefinitionVersionId": "f36dc264f14be3c6",
"latestAvailablePublishedVersion": 1,
"latestAvailablePublishedVersionId": "f36dc264f14be3c6",
"id": "377b23f4235ec26d",
"nodeId": "Workflow2:e6c941b6f2292fcf:377b23f4235ec26d",
"name": "GrandChild1",
"type": "GrandChild",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -193.95703125,
"y": 23
},
"size": {
"width": 115.1875,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,67 @@
{
"id": "f36dc264f14be3c6",
"definitionId": "29595e7b37a4836d",
"name": "Grand Child",
"createdAt": "2024-04-30T08:22:44.537237+00:00",
"version": 1,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "29044d8e2ba192c0",
"nodeId": "Workflow1:29044d8e2ba192c0",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Grand Child"
}
},
"id": "fb00878500fde1d3",
"nodeId": "Workflow1:29044d8e2ba192c0:fb00878500fde1d3",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -234.796875,
"y": -191
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,142 @@
{
"id": "9178b94f961b1cf",
"definitionId": "189be5173f90b1f6",
"name": "Parent",
"createdAt": "2024-04-30T08:42:17.838484+00:00",
"version": 6,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "96f60810318a6a89",
"nodeId": "Workflow3:96f60810318a6a89",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"path": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "parent"
}
},
"supportedMethods": {
"typeName": "String[]",
"expression": {
"type": "Object",
"value": "[\u0022POST\u0022]"
}
},
"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": "7e20498ffc94dbc6",
"nodeId": "Workflow3:96f60810318a6a89:7e20498ffc94dbc6",
"name": "HttpEndpoint1",
"type": "Elsa.HttpEndpoint",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -520,
"y": -120
},
"size": {
"width": 176.390625,
"height": 50
}
}
}
},
{
"workflowDefinitionId": "a3390d1f4c2594a8",
"workflowDefinitionVersionId": "26d186d68deb1250",
"latestAvailablePublishedVersion": 1,
"latestAvailablePublishedVersionId": "26d186d68deb1250",
"id": "ed6a03040f651906",
"nodeId": "Workflow3:96f60810318a6a89:ed6a03040f651906",
"name": "Child1",
"type": "Child",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -236.5810546875,
"y": -120
},
"size": {
"width": 67.7734375,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "7e20498ffc94dbc6",
"port": "Done"
},
"target": {
"activity": "ed6a03040f651906",
"port": "In"
}
}
]
}
}
@@ -0,0 +1,59 @@
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Models;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.ClusteredHosting;
public class ActivityRegistrySyncTests(App app) : AppComponentTest(app)
{
private readonly IServiceScope _pod1Scope = app.Cluster.Pod1.Services.CreateScope();
private readonly IServiceScope _pod2Scope = app.Cluster.Pod2.Services.CreateScope();
private readonly IServiceScope _pod3Scope = app.Cluster.Pod3.Services.CreateScope();
[Fact(Skip = "Not yet implemented")]
public async Task ImportWorkflowActivity_ShouldUpdateOtherPods()
{
var pod1ActivityRegistry = _pod1Scope.ServiceProvider.GetRequiredService<IActivityRegistry>();
var pod2ActivityRegistry = _pod2Scope.ServiceProvider.GetRequiredService<IActivityRegistry>();
var pod3ActivityRegistry = _pod3Scope.ServiceProvider.GetRequiredService<IActivityRegistry>();
var sub1Descriptor = pod1ActivityRegistry.Find("Sub");
Assert.Null(sub1Descriptor);
var importer = _pod1Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionImporter>();
var request = new SaveWorkflowDefinitionRequest
{
Model = new WorkflowDefinitionModel
{
Name = "Sub",
DefinitionId = "Sub",
Options = new WorkflowOptions
{
UsableAsActivity = true,
AutoUpdateConsumingWorkflows = true
}
},
Publish = true
};
await importer.ImportAsync(request);
sub1Descriptor = pod1ActivityRegistry.Find("Sub");
Assert.NotNull(sub1Descriptor);
sub1Descriptor = pod2ActivityRegistry.Find("Sub");
Assert.NotNull(sub1Descriptor);
sub1Descriptor = pod3ActivityRegistry.Find("Sub");
Assert.NotNull(sub1Descriptor);
}
protected override void OnDispose()
{
_pod1Scope.Dispose();
_pod2Scope.Dispose();
_pod3Scope.Dispose();
}
}
@@ -0,0 +1,36 @@
using Elsa.Common.Models;
using Elsa.Testing.Shared;
using Elsa.Testing.Shared.Services;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.ComponentTests.Scenarios.DispatchWorkflows.Workflows;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.DispatchWorkflows;
public class DispatchWorkflowsTests : AppComponentTest
{
private readonly SignalManager _signalManager;
private readonly IWorkflowRuntime _workflowRuntime;
public DispatchWorkflowsTests(App app) : base(app)
{
_workflowRuntime = Scope.ServiceProvider.GetRequiredService<IWorkflowRuntime>();
_signalManager = Scope.ServiceProvider.GetRequiredService<SignalManager>();
}
[Fact (Skip = "This test is flaky and needs to be fixed.")]
public async Task DispatchAndWaitWorkflow_ShouldWaitForChildWorkflowToComplete()
{
var workflowClient = await _workflowRuntime.CreateClientAsync();
await workflowClient.CreateInstanceAsync(new CreateWorkflowInstanceRequest
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(DispatchAndWaitWorkflow.DefinitionId, VersionOptions.Published)
});
await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
await _signalManager.WaitAsync<string>("Completed");
}
}
@@ -0,0 +1,23 @@
using Elsa.Scheduling.Activities;
using Elsa.Workflows.Activities;
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.DispatchWorkflows.Workflows;
[UsedImplicitly]
public class ChildWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
new Delay(TimeSpan.FromMilliseconds(250)),
new WriteLine("Hello from Child!")
}
};
}
}
@@ -0,0 +1,26 @@
using Elsa.Testing.Shared.Activities;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.DispatchWorkflows.Workflows;
public class DispatchAndWaitWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.Root = new Sequence
{
Activities =
{
new DispatchWorkflow
{
WorkflowDefinitionId = new(ChildWorkflow.DefinitionId),
WaitForCompletion = new (true)
},
new TriggerSignal("Completed")
}
};
}
}
@@ -0,0 +1,31 @@
using Elsa.Common.Models;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.ComponentTests.Scenarios.ExecuteWorkflows.Workflows;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.ExecuteWorkflows;
public class ExecuteWorkflowsTests : AppComponentTest
{
private readonly IWorkflowRuntime _workflowRuntime;
public ExecuteWorkflowsTests(App app) : base(app)
{
_workflowRuntime = Scope.ServiceProvider.GetRequiredService<IWorkflowRuntime>();
}
[Fact]
public async Task ExecuteWorkflow_ShouldExecuteWorkflow()
{
var workflowClient = await _workflowRuntime.CreateClientAsync();
await workflowClient.CreateInstanceAsync(new CreateWorkflowInstanceRequest
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(MainWorkflow.DefinitionId, VersionOptions.Published)
});
await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
}
}
@@ -0,0 +1,33 @@
using System.Text.Json;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.ExecuteWorkflows.Workflows;
public class MainWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var workflowResult = builder.WithVariable<ExecuteWorkflowResult>();
builder.Root = new Sequence
{
Activities =
{
new ExecuteWorkflow
{
WorkflowDefinitionId = new(SubroutineWorkflow.DefinitionId),
Input = new(new Dictionary<string, object>
{
["Value"] = 21
}),
Result = new(workflowResult)
},
new WriteLine(context => $"Subroutine output: {JsonSerializer.Serialize(workflowResult.Get(context))}")
}
};
}
}
@@ -0,0 +1,31 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Activities.SetOutput;
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.ExecuteWorkflows.Workflows;
[UsedImplicitly]
public class SubroutineWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var valueInput = builder.WithInput<double>("Value");
var output = builder.WithOutput<double>("Output");
builder.Root = new Sequence
{
Activities =
{
new WriteLine(context => $"Running subroutine on value {context.GetInput<double>(valueInput)}..."),
new SetOutput
{
OutputName = new(output.Name),
OutputValue = new(context => context.GetInput<double>(valueInput) * 2)
}
}
};
}
}
@@ -0,0 +1,15 @@
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows;
public class HttpHelloWorldTests(App app) : AppComponentTest(app)
{
[Fact]
public async Task HelloWorldWorkflow_ShouldRespondWithHelloWorld()
{
var client = WorkflowServer.CreateHttpWorkflowClient();
var response = await client.GetStringAsync("hello-world");
Assert.Equal("Hello World!", response);
}
}
@@ -0,0 +1,41 @@
using System.Net;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows;
public class ResumeSpecificHttpWorkflowInstanceTests(App app) : AppComponentTest(app)
{
[Theory]
[InlineData("workflowInstanceId")]
[InlineData("correlationId")]
public async Task ResumingSpecificWorkflow_ShouldResumeSpecifiedWorkflow(string identifierKey)
{
var client = WorkflowServer.CreateHttpWorkflowClient();
// Start 3 instances.
var workflowInstanceId1 = await StartWorkflowAsync(client, identifierKey);
var workflowInstanceId2 = await StartWorkflowAsync(client, identifierKey);
var workflowInstanceId3 = await StartWorkflowAsync(client, identifierKey);
// Resume the 2nd instance.
var response = await ResumeWorkflowAsync(client, identifierKey, workflowInstanceId2);
// Response should be OK.
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
private async Task<string> StartWorkflowAsync(HttpClient client, string identifierKey)
{
var identityGenerator = Scope.ServiceProvider.GetRequiredService<IIdentityGenerator>();
var identifierValue = identityGenerator.GenerateId();
await client.GetStringAsync($"simple-http-api/start?{identifierKey}={identifierValue}");
return identifierValue;
}
private async Task<HttpResponseMessage> ResumeWorkflowAsync(HttpClient client, string identifierKey, string identifierValue)
{
return await client.GetAsync($"simple-http-api/resume?{identifierKey}={identifierValue}");
}
}
@@ -0,0 +1,15 @@
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows;
public class RouteDataTests(App app) : AppComponentTest(app)
{
[Fact]
public async Task RouteDataWorkflow_ShouldRespondWithRouteData()
{
var client = WorkflowServer.CreateHttpWorkflowClient();
var response = await client.GetStringAsync("orders/42");
Assert.Equal("42", response);
}
}
@@ -0,0 +1,32 @@
using Elsa.Http;
using Elsa.Workflows.Activities;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows.Workflows;
public class RouteDataWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
var routeDataVariable = builder.WithVariable<IDictionary<string, object>>();
builder.Root = new Sequence
{
Activities =
[
new HttpEndpoint
{
Path = new("orders/{id}"),
SupportedMethods = new([HttpMethods.Get]),
CanStartWorkflow = true,
RouteData = new(routeDataVariable)
},
new WriteHttpResponse
{
Content = new(context => routeDataVariable.Get(context)!["id"].ToString()),
ContentType = new("text/plain")
}
]
};
}
}
@@ -0,0 +1,29 @@
using Elsa.Http;
using Elsa.Workflows.Activities;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.ComponentTests.Scenarios.HttpWorkflows.Workflows;
public class SimpleHttpApiWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder.Root = new Sequence
{
Activities =
[
new HttpEndpoint
{
Path = new("simple-http-api/start"),
SupportedMethods = new([HttpMethods.Get]),
CanStartWorkflow = true
},
new HttpEndpoint
{
Path = new("simple-http-api/resume"),
SupportedMethods = new([HttpMethods.Get])
}
]
};
}
}
@@ -0,0 +1,160 @@
{
"id": "a86a9f668817dcb6",
"definitionId": "f926beb0afc8505e",
"name": "HTTP Hello World",
"createdAt": "2024-04-20T17:14:34.118307+00:00",
"version": 2,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "95a12c8ee4e8c5fc",
"nodeId": "Workflow1:95a12c8ee4e8c5fc",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"path": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "hello-world"
}
},
"supportedMethods": {
"typeName": "String[]",
"expression": {
"type": "Object",
"value": "[\u0022GET\u0022]"
}
},
"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": "90c3d3b2ea1f6ddc",
"nodeId": "Workflow1:95a12c8ee4e8c5fc:90c3d3b2ea1f6ddc",
"name": "HttpEndpoint1",
"type": "Elsa.HttpEndpoint",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -262.796875,
"y": 264
},
"size": {
"width": 176.390625,
"height": 50
}
}
}
},
{
"statusCode": {
"typeName": "System.Net.HttpStatusCode, System.Net.Primitives",
"expression": {
"type": "Literal",
"value": "OK"
}
},
"content": {
"typeName": "Object",
"expression": {
"type": "Literal",
"value": "Hello World!"
}
},
"contentType": null,
"responseHeaders": {
"typeName": "Elsa.Http.Models.HttpHeaders, Elsa.Http",
"expression": {
"type": "Literal",
"value": "{}"
}
},
"id": "3048105347912af5",
"nodeId": "Workflow1:95a12c8ee4e8c5fc:3048105347912af5",
"name": "WriteHttpResponse1",
"type": "Elsa.WriteHttpResponse",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 35.203125,
"y": 264
},
"size": {
"width": 183.640625,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "90c3d3b2ea1f6ddc",
"port": "Done"
},
"target": {
"activity": "3048105347912af5",
"port": "In"
}
}
]
}
}
@@ -0,0 +1,25 @@
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.InputOutput;
public class InputOutputTests(App app) : AppComponentTest(app)
{
[Fact(DisplayName = "Input should be received by the Provider and should in turn be able to send it back to the Consumer.")]
public async Task InputShouldBeOutput()
{
const string workflowDefinitionVersionId = "4e993ab1b3d4e4a1";
var workflowDefinitionService = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionService>();
var workflowInvoker = Scope.ServiceProvider.GetRequiredService<IWorkflowInvoker>();
var variableManager = Scope.ServiceProvider.GetRequiredService<IWorkflowInstanceVariableManager>();
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionVersionId);
var result = await workflowInvoker.InvokeAsync(workflowGraph!);
var variables = await variableManager.GetVariablesAsync(result.WorkflowState.Id);
var variableValue = variables.First().Value;
Assert.Equal("Hello World!", variableValue);
}
}
@@ -0,0 +1,98 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "4e993ab1b3d4e4a1",
"definitionId": "58dbd7bffa85a761",
"name": "Consumer",
"createdAt": "2024-12-04T16:49:20.593932\u002B00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [
{
"id": "67fcb9870af4c28e",
"name": "Variable1",
"typeName": "String",
"isArray": false,
"value": "",
"storageDriverTypeName": "Elsa.Workflows.WorkflowInstanceStorageDriver, Elsa.Workflows.Core"
}
],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"logPersistenceConfig": {
"default": {
"evaluationMode": "Strategy",
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Inherit, Elsa.Workflows.Core",
"expression": null
}
}
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"id": "938f2c91bb759a6a",
"nodeId": "Workflow2:938f2c91bb759a6a",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {},
"activities": [
{
"workflowDefinitionId": "780fbf96f1bb1e7e",
"workflowDefinitionVersionId": "640867552fc865d",
"latestAvailablePublishedVersion": 3,
"latestAvailablePublishedVersionId": "640867552fc865d",
"id": "8c1127e1193cd6e8",
"nodeId": "Workflow2:938f2c91bb759a6a:8c1127e1193cd6e8",
"name": "Output1",
"type": "Provider",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -160,
"y": -280
},
"size": {
"width": 89.6953125,
"height": 50
}
}
},
"input1": {
"typeName": "Object",
"expression": {
"type": "Literal",
"value": "Hello World!"
},
"memoryReference": {
"id": "8c1127e1193cd6e8:input-input-1"
}
},
"output1": {
"typeName": "Object",
"memoryReference": {
"id": "67fcb9870af4c28e"
}
}
}
],
"variables": [],
"connections": []
}
}
@@ -0,0 +1,104 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "5ba5a7e36d60549b",
"definitionId": "780fbf96f1bb1e7e",
"name": "Provider",
"createdAt": "2024-12-04T16:22:39.064075\u002B00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [],
"inputs": [
{
"uiHint": "singleline",
"storageDriverType": "Elsa.Workflows.WorkflowInstanceStorageDriver, Elsa.Workflows.Core",
"type": "Object",
"name": "Input1",
"displayName": "Input 1",
"description": "",
"category": "Primitives",
"isArray": false
}
],
"outputs": [
{
"type": "Object",
"name": "Output1",
"displayName": "Output 1",
"description": "",
"category": "Primitives",
"isArray": false
}
],
"outcomes": [],
"customProperties": {
"logPersistenceConfig": {
"default": {
"evaluationMode": "Strategy",
"strategyType": "Elsa.Workflows.LogPersistence.Strategies.Inherit, Elsa.Workflows.Core",
"expression": null
}
}
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": false,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"id": "85a5e1d9e487bfa4",
"nodeId": "Workflow1:85a5e1d9e487bfa4",
"name": "Flowchart1",
"type": "Elsa.Flowchart",
"version": 1,
"customProperties": {
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {},
"activities": [
{
"outputName": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Output1"
}
},
"outputValue": {
"typeName": "Object",
"expression": {
"type": "JavaScript",
"value": "return getInput1()"
}
},
"id": "985eb3bfb8d4dd3f",
"nodeId": "Workflow1:85a5e1d9e487bfa4:985eb3bfb8d4dd3f",
"name": "SetOutput1",
"type": "Elsa.SetOutput",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -380,
"y": -220
},
"size": {
"width": 107.3203125,
"height": 50
}
}
}
}
],
"variables": [],
"connections": []
}
}
@@ -0,0 +1,16 @@
using Elsa.Extensions;
using Elsa.JavaScript.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.JavaScriptVariables;
public class JavaScriptVariablesWorkflow1 : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.WithVariable("MagicNumber", 3).WithWorkflowStorage();
builder.Root = new RunJavaScript("setMagicNumber(42)", null, null);
}
}
@@ -0,0 +1,16 @@
using Elsa.Extensions;
using Elsa.JavaScript.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.JavaScriptVariables;
public class JavaScriptVariablesWorkflow2 : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.WithVariable("MagicNumber", 3).WithWorkflowStorage();
builder.Root = new RunJavaScript("variables.MagicNumber = 42", null, null);
}
}
@@ -0,0 +1,16 @@
using Elsa.Extensions;
using Elsa.JavaScript.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.JavaScriptVariables;
public class JavaScriptVariablesWorkflow3 : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.WithVariable("MagicNumber", 3).WithWorkflowStorage();
builder.Root = new RunJavaScript("setVariable('MagicNumber', 42)", null, null);
}
}
@@ -0,0 +1,51 @@
using Elsa.Expressions.Helpers;
using Elsa.Extensions;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Management;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Elsa.Workflows.State;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.JavaScriptVariables;
public class JavaScriptVariablesWorkflowTests(App app) : AppComponentTest(app)
{
[Theory(DisplayName = "SetVariable JS function sets a variable and does not get overridden by variables API")]
[MemberData(nameof(GetWorkflowDefinitions))]
public async Task SetVariableRetainsValue(string workflowDefinitionId)
{
var workflowRuntime = Scope.ServiceProvider.GetRequiredService<IWorkflowRuntime>();
var workflowInstanceStore = Scope.ServiceProvider.GetRequiredService<IWorkflowInstanceStore>();
var workflowClient = await workflowRuntime.CreateClientAsync();
var runAndCreateRequest = new CreateAndRunWorkflowInstanceRequest
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(workflowDefinitionId)
};
var runResponse = await workflowClient.CreateAndRunInstanceAsync(runAndCreateRequest);
var workflowInstanceId = runResponse.WorkflowInstanceId;
var workflowInstance = await workflowInstanceStore.FindAsync(workflowInstanceId);
var workflowState = workflowInstance!.WorkflowState;
var rootWorkflowActivityExecutionContext = workflowState.ActivityExecutionContexts.Single(x => x.ParentContextId == null);
var variables = GetVariablesDictionary(rootWorkflowActivityExecutionContext);
var magicNumber = variables["magicNumberVariable"].ConvertTo<int>();
Assert.Equal(42, magicNumber);
}
public static IEnumerable<object[]> GetWorkflowDefinitions()
{
return
[
[JavaScriptVariablesWorkflow1.DefinitionId],
[JavaScriptVariablesWorkflow2.DefinitionId],
[JavaScriptVariablesWorkflow3.DefinitionId]
];
}
private VariablesDictionary GetVariablesDictionary(ActivityExecutionContextState context)
{
return context.Properties.GetOrAdd(WorkflowInstanceStorageDriver.VariablesDictionaryStateKey, () => new VariablesDictionary());
}
}
@@ -0,0 +1,109 @@
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Responses;
using Elsa.Api.Client.Resources.WorkflowInstances.Models;
using Elsa.Common.Entities;
using Elsa.Testing.Shared.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.OrderDefinitions;
using Microsoft.Extensions.DependencyInjection;
using Open.Linq.AsyncExtensions;
namespace Elsa.Workflows.ComponentTests.Scenarios.LogPersistenceModes;
public class InputOutputLoggingTests(App app) : AppComponentTest(app)
{
[Theory]
[InlineData("input-output-logging-1", true, false, true, false)]
[InlineData("input-output-logging-2", true, false, true, false)]
public async Task Workflow_ShouldHonorSettings_WhenExecuting(string workflowDefinitionId, params bool[] shouldBeIncludedArray)
{
var workflowState = await ExecuteWorkflowAsync(workflowDefinitionId);
var workflowInstanceId = workflowState.Id;
var writeLineActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<WriteLine>();
var activityExecutionRecords = await GetActivityExecutionRecordsAsync(workflowInstanceId, x => x.ActivityType == writeLineActivityTypeName).ToList();
for (var i = 0; i < shouldBeIncludedArray.Length; i++)
{
var activityExecutionRecord = activityExecutionRecords[i];
var shouldBeIncluded = shouldBeIncludedArray[i];
var isIncluded = activityExecutionRecord.ActivityState?.ContainsKey(nameof(WriteLine.Text)) == true;
Assert.Equal(shouldBeIncluded, isIncluded);
}
}
[Fact]
public async Task WorkflowAsActivity_ShouldHonorSettings_WhenExecuting()
{
await ExecuteWorkflowAsync("input-output-logging-3");
var activityExecutionRecord = await GetRecordByActivityNameAsync("InputOutputLogging3-Child1");
var output1IsIncluded = activityExecutionRecord.Outputs!.ContainsKey("Output1");
var output2IsIncluded = activityExecutionRecord.Outputs!.ContainsKey("Output2");
Assert.True(output1IsIncluded);
Assert.True(output2IsIncluded);
}
[Fact(Skip = "Although this functionality works in practice, the component test fails from time to time for no clear reason (yet).")]
public async Task WorkflowAsActivityInternal_ShouldHonorSettings_WhenExecuting()
{
await ExecuteWorkflowAsync("input-output-logging-3");
var setOutput1Record = await GetRecordByActivityNameAsync("SetOutput1");
var setOutput2Record = await GetRecordByActivityNameAsync("SetOutput2");
var output1IsIncluded = setOutput1Record.ActivityState?.ContainsKey("OutputName") == true;
var output2IsIncluded = setOutput2Record.ActivityState?.ContainsKey("OutputName") == true;
Assert.False(output1IsIncluded);
Assert.True(output2IsIncluded);
}
private async Task<WorkflowState> ExecuteWorkflowAsync(string workflowDefinitionId)
{
var client = WorkflowServer.CreateApiClient<IExecuteWorkflowApi>();
using var response = await client.ExecuteAsync(workflowDefinitionId);
var model = await response.ReadAsJsonAsync<ExecuteWorkflowDefinitionResponse>(WorkflowServer.Services);
return model.WorkflowState;
}
private Task<IEnumerable<ActivityExecutionRecord>> GetActivityExecutionRecordsAsync(string workflowInstanceId, Func<ActivityExecutionRecord, bool> predicate)
{
var activityExecutionRecordFilter = new ActivityExecutionRecordFilter
{
WorkflowInstanceId = workflowInstanceId,
};
return GetActivityExecutionRecordsAsync(activityExecutionRecordFilter, predicate);
}
private Task<ActivityExecutionRecord> GetRecordByActivityNameAsync(string name)
{
var filter = new ActivityExecutionRecordFilter
{
Name = name
};
return GetActivityExecutionRecordAsync(filter);
}
private async Task<IEnumerable<ActivityExecutionRecord>> GetActivityExecutionRecordsAsync(ActivityExecutionRecordFilter filter, Func<ActivityExecutionRecord, bool>? predicate = null)
{
var activityExecutionStore = Scope.ServiceProvider.GetRequiredService<IActivityExecutionStore>();
var orderBy = new ActivityExecutionRecordOrder<DateTimeOffset>(x => x.StartedAt, OrderDirection.Ascending);
var activityExecutionRecords = await activityExecutionStore.FindManyAsync(filter, orderBy);
return predicate == null ? activityExecutionRecords : activityExecutionRecords.Where(predicate);
}
private async Task<ActivityExecutionRecord> GetActivityExecutionRecordAsync(ActivityExecutionRecordFilter filter)
{
var activityExecutionStore = Scope.ServiceProvider.GetRequiredService<IActivityExecutionStore>();
var result = await activityExecutionStore.FindAsync(filter);
if(result == null)
throw new InvalidOperationException($"Activity execution record not found.");
return result;
}
}
@@ -0,0 +1,210 @@
{
"id": "94f11e583299383b",
"definitionId": "input-output-logging-1",
"name": "Input Output Logging 1",
"createdAt": "2024-04-21T11:34:17.423214+00:00",
"version": 1,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "482993d55270a5e7",
"nodeId": "Workflow1:482993d55270a5e7",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "This input should be stored"
}
},
"id": "ea21b3863867ebb3",
"nodeId": "Workflow1:482993d55270a5e7:ea21b3863867ebb3",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -440,
"y": -140
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "This input should not be stored"
}
},
"id": "4726389b3d516c40",
"nodeId": "Workflow1:482993d55270a5e7:4726389b3d516c40",
"name": "WriteLine2",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"logPersistenceMode": {
"default": "Exclude",
"inputs": {
"text": "Inherit"
},
"outputs": {}
},
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -220,
"y": -140
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "This input should be stored"
}
},
"id": "e13b09839d4244b9",
"nodeId": "Workflow1:482993d55270a5e7:e13b09839d4244b9",
"name": "WriteLine3",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"logPersistenceMode": {
"default": "Exclude",
"inputs": {
"text": "Include"
},
"outputs": {}
},
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -16,
"y": -140
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "This input should not be stored"
}
},
"id": "1ba2922db9052ce9",
"nodeId": "Workflow1:482993d55270a5e7:1ba2922db9052ce9",
"name": "WriteLine4",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"logPersistenceMode": {
"default": "Include",
"inputs": {
"text": "Exclude"
},
"outputs": {}
},
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 188,
"y": -140
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "ea21b3863867ebb3",
"port": "Done"
},
"target": {
"activity": "4726389b3d516c40",
"port": "In"
}
},
{
"source": {
"activity": "4726389b3d516c40",
"port": "Done"
},
"target": {
"activity": "e13b09839d4244b9",
"port": "In"
}
},
{
"source": {
"activity": "e13b09839d4244b9",
"port": "Done"
},
"target": {
"activity": "1ba2922db9052ce9",
"port": "In"
}
}
]
}
}
@@ -0,0 +1,213 @@
{
"id": "a7499c4c7888fafb",
"definitionId": "input-output-logging-2",
"name": "Input Output Logging 2",
"createdAt": "2024-04-21T11:39:57.182662+00:00",
"version": 2,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": [],
"logPersistenceMode": {
"default": "Exclude"
}
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "4dce3806d9c7af92",
"nodeId": "Workflow1:4dce3806d9c7af92",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "This should be logged"
}
},
"id": "30d32d634c1ddbfa",
"nodeId": "Workflow1:4dce3806d9c7af92:30d32d634c1ddbfa",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"logPersistenceMode": {
"default": "Include",
"inputs": {
"text": "Inherit"
},
"outputs": {}
},
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -380,
"y": -120
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "This should not be logged"
}
},
"id": "50fcb804a7ace442",
"nodeId": "Workflow1:4dce3806d9c7af92:50fcb804a7ace442",
"name": "WriteLine2",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -156,
"y": -120
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "This should be logged"
}
},
"id": "3f00f1de3ec2862b",
"nodeId": "Workflow1:4dce3806d9c7af92:3f00f1de3ec2862b",
"name": "WriteLine3",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"logPersistenceMode": {
"default": "Inherit",
"inputs": {
"text": "Include"
},
"outputs": {}
},
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 60,
"y": -120
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "This should not be logged"
}
},
"id": "3dcff307781b3707",
"nodeId": "Workflow1:4dce3806d9c7af92:3dcff307781b3707",
"name": "WriteLine4",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"logPersistenceMode": {
"default": "Inherit",
"inputs": {
"text": "Exclude"
},
"outputs": {}
},
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 264,
"y": -120
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "3f00f1de3ec2862b",
"port": "Done"
},
"target": {
"activity": "3dcff307781b3707",
"port": "In"
}
},
{
"source": {
"activity": "30d32d634c1ddbfa",
"port": "Done"
},
"target": {
"activity": "50fcb804a7ace442",
"port": "In"
}
},
{
"source": {
"activity": "50fcb804a7ace442",
"port": "Done"
},
"target": {
"activity": "3f00f1de3ec2862b",
"port": "In"
}
}
]
}
}
@@ -0,0 +1,158 @@
{
"id": "4cf7c10c7a2901d6",
"definitionId": "input-output-logging-3-child",
"name": "Input Output Logging 3 - Child",
"createdAt": "2024-04-22T13:11:07.146222+00:00",
"version": 4,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [
{
"type": "Object",
"name": "Output1",
"displayName": "Output 1",
"category": "Primitives",
"isArray": false
},
{
"type": "Object",
"name": "Output2",
"displayName": "Output 2",
"category": "Primitives",
"isArray": false
}
],
"outcomes": [],
"customProperties": {
"logPersistenceMode": {
"default": "Exclude"
},
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "5f72efc9043753fa",
"nodeId": "Workflow1:5f72efc9043753fa",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"outputName": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Output1"
}
},
"outputValue": {
"typeName": "Object",
"expression": {
"type": "Literal",
"value": "Output value 1"
}
},
"id": "402076f12c1220a9",
"nodeId": "Workflow1:5f72efc9043753fa:402076f12c1220a9",
"name": "SetOutput1",
"type": "Elsa.SetOutput",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false,
"logPersistenceMode": {
"default": "Inherit",
"inputs": {
"outputName": "Inherit",
"outputValue": "Inherit"
},
"outputs": {}
}
},
"metadata": {
"designer": {
"position": {
"x": -221.4757080078125,
"y": 264.861083984375
},
"size": {
"width": 107.734375,
"height": 50.2256965637207
}
}
}
},
{
"outputName": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Output2"
}
},
"outputValue": {
"typeName": "Object",
"expression": {
"type": "Literal",
"value": "Output value 2"
}
},
"id": "db1d1c02cbfac590",
"nodeId": "Workflow1:5f72efc9043753fa:db1d1c02cbfac590",
"name": "SetOutput2",
"type": "Elsa.SetOutput",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false,
"logPersistenceMode": {
"default": "Inherit",
"inputs": {
"outputName": "Include",
"outputValue": "Include"
},
"outputs": {}
}
},
"metadata": {
"designer": {
"position": {
"x": 58.5242919921875,
"y": 264.861083984375
},
"size": {
"width": 107.734375,
"height": 50.2256965637207
}
}
}
}
],
"connections": [
{
"source": {
"activity": "402076f12c1220a9",
"port": "Done"
},
"target": {
"activity": "db1d1c02cbfac590",
"port": "In"
}
}
]
}
}
@@ -0,0 +1,65 @@
{
"id": "6da18530316aee2e",
"definitionId": "input-output-logging-3",
"name": "Input Output Logging 3",
"createdAt": "2024-04-21T20:08:17.495551+00:00",
"version": 1,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "9aec3fea5a39cccd",
"nodeId": "Workflow2:9aec3fea5a39cccd",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"workflowDefinitionId": "input-output-logging-3-child",
"workflowDefinitionVersionId": "4cf7c10c7a2901d6",
"latestAvailablePublishedVersion": 4,
"latestAvailablePublishedVersionId": "4cf7c10c7a2901d6",
"id": "6bf4016d6074831f",
"nodeId": "Workflow2:9aec3fea5a39cccd:6bf4016d6074831f",
"name": "InputOutputLogging3-Child1",
"type": "InputOutputLogging3-Child",
"version": 4,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -360,
"y": -320
},
"size": {
"width": 246.375,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,24 @@
using Elsa.Common.Models;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.Multitenancy;
public class MultitenancyTests(App app) : AppComponentTest(app)
{
[Fact(Skip = "Multitenancy disabled. This test doesn't work because not all workflows are assigned the Tenant1 tenant.")]
public async Task LoadingWorkflows_ShouldReturnWorkflows_FromCurrentTenant()
{
var store = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionStore>();
var filter = new WorkflowDefinitionFilter
{
IsSystem = false,
VersionOptions = VersionOptions.Latest
};
var workflows = await store.FindManyAsync(filter);
Assert.All(workflows, workflow => Assert.Equal("Tenant1", workflow.TenantId));
}
}
@@ -0,0 +1,16 @@
using Elsa.Workflows.Activities;
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.Multitenancy.Workflows;
[UsedImplicitly]
public class WorkflowForTenant1 : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.WithTenantId("Tenant1");
builder.Root = new WriteLine("Tenant1");
}
}
@@ -0,0 +1,16 @@
using Elsa.Workflows.Activities;
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.Multitenancy.Workflows;
[UsedImplicitly]
public class WorkflowForTenant2 : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
builder.WithTenantId("Tenant2");
builder.Root = new WriteLine("Tenant2");
}
}
@@ -0,0 +1,13 @@
using Elsa.Extensions;
namespace Elsa.Workflows.ComponentTests.Scenarios.Variables.Activities;
public class CountdownStep : Activity
{
protected override void Execute(ActivityExecutionContext context)
{
var counter = context.GetVariable<int>("Counter");
context.SetVariable("Counter", counter - 1);
context?.CreateBookmark();
}
}
@@ -0,0 +1,71 @@
using Elsa.Common.Models;
using Elsa.Expressions.Helpers;
using Elsa.Extensions;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.ComponentTests.Scenarios.Variables.Workflows;
using Elsa.Workflows.Management;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.Messages;
using Elsa.Workflows.State;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.Variables;
public class CountdownWorkflowTests(App app) : AppComponentTest(app)
{
[Fact(DisplayName = "Variable is persisted across workflow runs")]
public async Task VariableIsPersistedAcrossWorkflowRuns()
{
var workflowRuntime = Scope.ServiceProvider.GetRequiredService<IWorkflowRuntime>();
var workflowClient = await workflowRuntime.CreateClientAsync();
var workflowInstanceStore = Scope.ServiceProvider.GetRequiredService<IWorkflowInstanceStore>();
var bookmarkStore = Scope.ServiceProvider.GetRequiredService<IBookmarkStore>();
var runAndCreateRequest = new CreateAndRunWorkflowInstanceRequest
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(CountdownWorkflow.DefinitionId, VersionOptions.Latest),
};
var runResponse = await workflowClient.CreateAndRunInstanceAsync(runAndCreateRequest);
var workflowInstanceId = runResponse.WorkflowInstanceId;
var createdBookmarks = await bookmarkStore.FindManyAsync(new()
{
WorkflowInstanceId = workflowInstanceId
});
var bookmarks = new Stack<StoredBookmark>(createdBookmarks);
var expectedCounter = 3;
while (bookmarks.Any())
{
var workflowInstance = await workflowInstanceStore.FindAsync(workflowInstanceId);
var workflowState = workflowInstance!.WorkflowState;
var rootWorkflowActivityExecutionContext = workflowState.ActivityExecutionContexts.Single(x => x.ParentContextId == null);
var variables = GetVariablesDictionary(rootWorkflowActivityExecutionContext);
var actualCounter = variables["counterVariable"].ConvertTo<int>();
Assert.Equal(--expectedCounter, actualCounter);
var bookmark = bookmarks.Pop();
var runRequest = new RunWorkflowInstanceRequest
{
BookmarkId = bookmark.Id,
};
await workflowClient.RunInstanceAsync(runRequest);
createdBookmarks = await bookmarkStore.FindManyAsync(new()
{
WorkflowInstanceId = workflowInstanceId
});
foreach (var newBookmark in createdBookmarks)
bookmarks.Push(newBookmark);
}
}
private VariablesDictionary GetVariablesDictionary(ActivityExecutionContextState context)
{
return context.Properties.GetOrAdd(WorkflowInstanceStorageDriver.VariablesDictionaryStateKey, () => new VariablesDictionary());
}
}
@@ -0,0 +1,34 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.ComponentTests.Scenarios.Variables.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.Variables.Workflows;
public class CountdownWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var counter = builder.WithVariable("Counter", 3).WithWorkflowStorage();
builder.Root = new Sequence
{
Activities =
{
new While(context => counter.Get(context) > 0)
{
Body = new Sequence
{
Activities =
{
new WriteLine(context => $"Counter: {counter.Get(context)}"),
new CountdownStep()
}
}
}
}
};
}
}
@@ -0,0 +1,100 @@
using Elsa.Http;
using Elsa.Testing.Shared;
using Elsa.Testing.Shared.Services;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Runtime.Filters;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities;
public class AutoUpdateTests : AppComponentTest
{
private const string ParentDefinitionVersionId = "4b584e249fdca951";
private const string ParentDefinitionId = "878770f04439a55d";
private const string ChildDefinitionId = "f353742a9ef6af4";
private static readonly object HttpChangeTokenSignal = new();
private static readonly object TriggerChangeTokenSignal = new();
private static readonly object GraphChangeTokenSignal = new();
private readonly IMemoryCache _cache;
private readonly TriggerChangeTokenSignalEvents _changeTokenEvents;
private readonly IWorkflowDefinitionCacheManager _definitionCacheManager;
private readonly IHasher _hasher;
private readonly IHttpWorkflowsCacheManager _httpCacheManager;
private readonly IWorkflowDefinitionPublisher _publisher;
private readonly SignalManager _signalManager;
private readonly IWorkflowDefinitionCacheManager _workflowCacheManager;
private string? _graphChangeToken;
private string? _httpChangeToken;
private string? _triggerChangeToken;
public AutoUpdateTests(App app) : base(app)
{
_cache = Scope.ServiceProvider.GetRequiredService<IMemoryCache>();
_hasher = Scope.ServiceProvider.GetRequiredService<IHasher>();
_definitionCacheManager = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionCacheManager>();
_publisher = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionPublisher>();
_httpCacheManager = Scope.ServiceProvider.GetRequiredService<IHttpWorkflowsCacheManager>();
_workflowCacheManager = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionCacheManager>();
_signalManager = Scope.ServiceProvider.GetRequiredService<SignalManager>();
_changeTokenEvents = Scope.ServiceProvider.GetRequiredService<TriggerChangeTokenSignalEvents>();
_changeTokenEvents.ChangeTokenSignalTriggered += OnChangeTokenSignalTriggered;
}
[Fact(DisplayName = "Updating a workflow with `auto update consuming workflows` should invalidate consuming workflows from cache")]
public async Task UpdateWorkflowWithAutoUpdate()
{
// Run workflow to make sure the all required items for running the workflow are in the cache.
var client = WorkflowServer.CreateHttpWorkflowClient();
await client.GetStringAsync("test-cache-invalidation");
// Make sure the items are in the cache.
var hash = _httpCacheManager.ComputeBookmarkHash("/test-cache-invalidation", "get");
Assert.True(_cache.TryGetValue($"http-workflow:{hash}", out _));
var filter = new TriggerFilter
{
Hash = hash
};
var hashedFilter = _hasher.Hash(filter);
Assert.True(_cache.TryGetValue($"IEnumerable`1:{hashedFilter}", out _));
var parentWorkflowDefinitionFilter = new WorkflowDefinitionFilter
{
Id = ParentDefinitionVersionId
};
var parentVersionCacheKey = _definitionCacheManager.CreateWorkflowFilterCacheKey(parentWorkflowDefinitionFilter);
Assert.True(_cache.TryGetValue(parentVersionCacheKey, out _));
// Set change tokens.
_httpChangeToken = _workflowCacheManager.CreateWorkflowDefinitionChangeTokenKey(ParentDefinitionId);
_triggerChangeToken = _httpCacheManager.GetTriggerChangeTokenKey(hash);
_graphChangeToken = _workflowCacheManager.CreateWorkflowDefinitionChangeTokenKey(ParentDefinitionId);
// (Act) Save the draft version of the child workflow and update the references.
await _publisher.PublishAsync(ChildDefinitionId);
// Wait until the notifications for updating the cache have been send and check the cache.
await _signalManager.WaitAsync<TriggerChangeTokenSignalEventArgs>(HttpChangeTokenSignal);
await _signalManager.WaitAsync<TriggerChangeTokenSignalEventArgs>(TriggerChangeTokenSignal);
await _signalManager.WaitAsync<TriggerChangeTokenSignalEventArgs>(GraphChangeTokenSignal);
Assert.False(_cache.TryGetValue($"http-workflow:{hash}", out _));
Assert.False(_cache.TryGetValue($"IEnumerable`1:{hashedFilter}", out _));
Assert.False(_cache.TryGetValue(parentVersionCacheKey, out _));
}
private void OnChangeTokenSignalTriggered(object? sender, TriggerChangeTokenSignalEventArgs args)
{
if (args.Key == _httpChangeToken) _signalManager.Trigger(HttpChangeTokenSignal, args);
if (args.Key == _triggerChangeToken) _signalManager.Trigger(TriggerChangeTokenSignal, args);
if (args.Key == _graphChangeToken) _signalManager.Trigger(GraphChangeTokenSignal, args);
}
}
@@ -0,0 +1,84 @@
using Elsa.Testing.Shared;
using Elsa.Testing.Shared.Services;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities.Workflows;
using Elsa.Workflows.Management;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities;
public class DeleteWorkflowTests : AppComponentTest
{
private static readonly object WorkflowDeletedSignal = new();
private readonly IServiceScope _scope1;
// private readonly IServiceScope _scope2;
// private readonly IServiceScope _scope3;
private readonly SignalManager _signalManager;
private readonly WorkflowDefinitionEvents _workflowDefinitionEvents;
public DeleteWorkflowTests(App app) : base(app)
{
_scope1 = app.Cluster.Pod1.Services.CreateScope();
// Disabled these scope creations since this prevents events from firing in other tests.
// _scope2 = app.Cluster.Pod2.Services.CreateScope();
// _scope3 = app.Cluster.Pod3.Services.CreateScope();
_signalManager = Scope.ServiceProvider.GetRequiredService<SignalManager>();
_workflowDefinitionEvents = Scope.ServiceProvider.GetRequiredService<WorkflowDefinitionEvents>();
_workflowDefinitionEvents.WorkflowDefinitionDeleted += OnWorkflowDefinitionDeleted;
}
[Fact]
public async Task DeleteWorkflow()
{
EnsureWorkflowInRegistry(_scope1, Workflows.DeleteWorkflow.Type);
var workflowDefinitionManager = _scope1.ServiceProvider.GetRequiredService<IWorkflowDefinitionManager>();
await workflowDefinitionManager.DeleteByDefinitionIdAsync(Workflows.DeleteWorkflow.DefinitionId);
WorkflowTypeDeletedFromRegistry(_scope1, Workflows.DeleteWorkflow.Type);
}
[Fact(Skip = "Clustered tests are interfering with other event driven tests")]
public async Task DeleteWorkflow_Clustered()
{
EnsureWorkflowInRegistry(_scope1, DeleteWorkflowClustered.Type);
// EnsureWorkflowInRegistry(_scope2, DeleteWorkflowClustered.Type);
// EnsureWorkflowInRegistry(_scope3, DeleteWorkflowClustered.Type);
var workflowDefinitionManager = _scope1.ServiceProvider.GetRequiredService<IWorkflowDefinitionManager>();
await workflowDefinitionManager.DeleteByDefinitionIdAsync(DeleteWorkflowClustered.DefinitionId);
WorkflowTypeDeletedFromRegistry(_scope1, DeleteWorkflowClustered.Type);
await _signalManager.WaitAsync<WorkflowDefinitionDeletedEventArgs>(WorkflowDeletedSignal);
// WorkflowTypeDeletedFromRegistry(_scope2, DeleteWorkflowClustered.Type);
// WorkflowTypeDeletedFromRegistry(_scope3, DeleteWorkflowClustered.Type);
}
private static void EnsureWorkflowInRegistry(IServiceScope scope, string type)
{
var activityRegistry = scope.ServiceProvider.GetRequiredService<IActivityRegistry>();
var descriptor = activityRegistry.Find(type);
Assert.NotNull(descriptor);
}
private static void WorkflowTypeDeletedFromRegistry(IServiceScope scope, string type)
{
var activityRegistry = scope.ServiceProvider.GetRequiredService<IActivityRegistry>();
var descriptor = activityRegistry.Find(type);
Assert.Null(descriptor);
}
private void OnWorkflowDefinitionDeleted(object? sender, WorkflowDefinitionDeletedEventArgs args)
{
if (args.DefinitionId == Workflows.DeleteWorkflow.DefinitionId)
_signalManager.Trigger(WorkflowDeletedSignal, args);
}
protected override void OnDispose()
{
_workflowDefinitionEvents.WorkflowDefinitionDeleted -= OnWorkflowDefinitionDeleted;
}
}
@@ -0,0 +1,55 @@
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Models;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities;
public class SaveWorkflowTests(App app) : AppComponentTest(app)
{
[Theory(DisplayName = "Saving workflows updates ActivityRegistry")]
[InlineData("Save1", true, true, true, true)]
[InlineData("Save2", true, false, false, false)]
[InlineData("Save3", false, true, false, false)]
[InlineData("Save4", false, false, false, false)]
public async Task ActivityRegistry(string name, bool usableAsActivity, bool publish, bool expectedInRegistry, bool isBrowsable)
{
var activityRegistry = Scope.ServiceProvider.GetRequiredService<IActivityRegistry>();
var descriptor = activityRegistry.Find(name);
if (descriptor is not null)
activityRegistry.Remove(typeof(WorkflowDefinitionActivityProvider), descriptor);
var importer = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionImporter>();
var request = new SaveWorkflowDefinitionRequest
{
Model = new WorkflowDefinitionModel
{
Name = name,
DefinitionId = name,
Options = new WorkflowOptions
{
UsableAsActivity = usableAsActivity,
AutoUpdateConsumingWorkflows = true
}
},
Publish = publish
};
await importer.ImportAsync(request);
descriptor = activityRegistry.Find(name);
if (expectedInRegistry)
{
Assert.NotNull(descriptor);
Assert.Equal(isBrowsable, descriptor.IsBrowsable);
}
else
{
Assert.Null(descriptor);
}
}
}
@@ -0,0 +1,67 @@
{
"id": "23d18a46dc3000fd",
"definitionId": "f353742a9ef6af4",
"name": "CacheInvalidation_Child",
"createdAt": "2024-05-28T08:10:31.5429337+00:00",
"version": 1,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "5d146ccb42668b6b",
"nodeId": "Workflow1:5d146ccb42668b6b",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"correlationId": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Child version 1"
}
},
"id": "d719079757f5d1bf",
"nodeId": "Workflow1:5d146ccb42668b6b:d719079757f5d1bf",
"name": "Correlate1",
"type": "Elsa.Correlate",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -127.40211486816406,
"y": 526.2000122070312
},
"size": {
"width": 132.7375030517578,
"height": 49.60000228881836
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,67 @@
{
"id": "3da9c310a071ac7b",
"definitionId": "f353742a9ef6af4",
"name": "CacheInvalidation_Child",
"createdAt": "2024-05-28T11:00:47.8186457+00:00",
"version": 2,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": false,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "5d146ccb42668b6b",
"nodeId": "Workflow1:5d146ccb42668b6b",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"correlationId": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Child version 2"
}
},
"id": "d719079757f5d1bf",
"nodeId": "Workflow1:5d146ccb42668b6b:d719079757f5d1bf",
"name": "Correlate1",
"type": "Elsa.Correlate",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -127.40211486816406,
"y": 526.2000122070312
},
"size": {
"width": 132.7375030517578,
"height": 49.60000228881836
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,188 @@
{
"id": "4b584e249fdca951",
"definitionId": "878770f04439a55d",
"name": "CacheInvalidation_Parent",
"createdAt": "2024-05-28T08:11:06.1123531+00:00",
"version": 1,
"toolVersion": "3.2.0.0",
"variables": [
{
"id": "47ab4de953db266",
"name": "CorrId",
"typeName": "String",
"isArray": false,
"storageDriverTypeName": "Elsa.Workflows.Services.WorkflowStorageDriver, Elsa.Workflows.Core"
}
],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "76f03074bbc8cff3",
"nodeId": "Workflow2:76f03074bbc8cff3",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"workflowDefinitionId": "f353742a9ef6af4",
"workflowDefinitionVersionId": "23d18a46dc3000fd",
"latestAvailablePublishedVersion": 1,
"latestAvailablePublishedVersionId": "23d18a46dc3000fd",
"id": "b8fe7116016141f",
"nodeId": "Workflow2:76f03074bbc8cff3:b8fe7116016141f",
"name": "CacheInvalidationChild1",
"type": "CacheInvalidationChild",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -40,
"y": 320
},
"size": {
"width": 203.9499969482422,
"height": 49.60000228881836
}
}
}
},
{
"value": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Parent version 1"
}
},
"id": "ad9ee49c807b852e",
"nodeId": "Workflow2:76f03074bbc8cff3:ad9ee49c807b852e",
"name": "SetName1",
"type": "Elsa.SetName",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -303.3261413574219,
"y": 320
},
"size": {
"width": 137.6354217529297,
"height": 49.60000228881836
}
}
}
},
{
"path": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "test-cache-invalidation"
}
},
"supportedMethods": {
"typeName": "String[]",
"expression": {
"type": "Object",
"value": "[\u0022GET\u0022]"
}
},
"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": "109b9c25e55375ba",
"nodeId": "Workflow2:76f03074bbc8cff3:109b9c25e55375ba",
"name": "HttpEndpoint1",
"type": "Elsa.HttpEndpoint",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -622.4391784667969,
"y": 320
},
"size": {
"width": 176.0625,
"height": 49.60000228881836
}
}
}
}
],
"connections": [
{
"source": {
"activity": "ad9ee49c807b852e",
"port": "Done"
},
"target": {
"activity": "b8fe7116016141f",
"port": "In"
}
},
{
"source": {
"activity": "109b9c25e55375ba",
"port": "Done"
},
"target": {
"activity": "ad9ee49c807b852e",
"port": "In"
}
}
]
}
}
@@ -0,0 +1,25 @@
using Elsa.Scheduling.Activities;
using Elsa.Workflows.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities.Workflows;
public class DeleteWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
public static readonly string Type = nameof(DeleteWorkflow);
protected override void Build(IWorkflowBuilder builder)
{
builder.Name = Type;
builder.WithDefinitionId(DefinitionId);
builder.WorkflowOptions.UsableAsActivity = true;
builder.Root = new Sequence
{
Activities =
{
new Delay(TimeSpan.FromMilliseconds(250)),
new WriteLine("This workflow will be deleted!")
}
};
}
}
@@ -0,0 +1,25 @@
using Elsa.Scheduling.Activities;
using Elsa.Workflows.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowActivities.Workflows;
public class DeleteWorkflowClustered : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
public static readonly string Type = nameof(DeleteWorkflowClustered);
protected override void Build(IWorkflowBuilder builder)
{
builder.Name = Type;
builder.WithDefinitionId(DefinitionId);
builder.WorkflowOptions.UsableAsActivity = true;
builder.Root = new Sequence
{
Activities =
{
new Delay(TimeSpan.FromMilliseconds(250)),
new WriteLine("This workflow will be deleted!")
}
};
}
}
@@ -0,0 +1,23 @@
using System.Net;
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Testing.Shared.Extensions;
using Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Execute;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowCompletion;
public class WorkflowCompletionTests(App app) : AppComponentTest(app)
{
[Theory]
[InlineData("2630068018ac1f0a")]
[InlineData("5590069018aa4f0e")]
public async Task Workflow_ShouldComplete(string workflowDefinitionId)
{
var client = WorkflowServer.CreateApiClient<IExecuteWorkflowApi>();
using var response = await client.ExecuteAsync(workflowDefinitionId);
var model = await response.ReadAsJsonAsync<Response>(WorkflowServer.Services);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(WorkflowSubStatus.Finished, model.WorkflowState.SubStatus);
}
}
@@ -0,0 +1,189 @@
{
"id": "5590069018aa4f0e",
"definitionId": "5590069018aa4f0e",
"name": "Fork",
"createdAt": "2024-04-20T20:52:32.29327+00:00",
"version": 2,
"toolVersion": "3.2.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "c74056c7f1f4a3",
"nodeId": "Workflow1:c74056c7f1f4a3",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"branches": {
"typeName": "String[]",
"expression": {
"type": "Object",
"value": "[\u0022One\u0022,\u0022Two\u0022,\u0022Three\u0022]"
}
},
"id": "c0e09e3ac3cfdba7",
"nodeId": "Workflow1:c74056c7f1f4a3:c0e09e3ac3cfdba7",
"name": "FlowFork1",
"type": "Elsa.FlowFork",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -340,
"y": -197
},
"size": {
"width": 145.5625,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "One"
}
},
"id": "e6f0cddb1cdbdd7c",
"nodeId": "Workflow1:c74056c7f1f4a3:e6f0cddb1cdbdd7c",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -80,
"y": -300
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Two"
}
},
"id": "3ece093dfc20c752",
"nodeId": "Workflow1:c74056c7f1f4a3:3ece093dfc20c752",
"name": "WriteLine2",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -80,
"y": -197
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Three"
}
},
"id": "ca9fcd1d6069df9",
"nodeId": "Workflow1:c74056c7f1f4a3:ca9fcd1d6069df9",
"name": "WriteLine3",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -80,
"y": -102
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "c0e09e3ac3cfdba7",
"port": "One"
},
"target": {
"activity": "e6f0cddb1cdbdd7c",
"port": "In"
}
},
{
"source": {
"activity": "c0e09e3ac3cfdba7",
"port": "Two"
},
"target": {
"activity": "3ece093dfc20c752",
"port": "In"
}
},
{
"source": {
"activity": "c0e09e3ac3cfdba7",
"port": "Three"
},
"target": {
"activity": "ca9fcd1d6069df9",
"port": "In"
}
}
]
}
}
@@ -0,0 +1,42 @@
{
"id": "2630068018ac1f0a",
"definitionId": "2630068018ac1f0a",
"name": "Hello World",
"isLatest": true,
"isPublished": true,
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "969b0703a9379c3b",
"nodeId": "Workflow1:969b0703a9379c3b",
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Hello World!"
}
},
"id": "b039045bb7443e57",
"nodeId": "Workflow1:969b0703a9379c3b:b039045bb7443e57",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"metadata": {
"designer": {
"position": {
"x": -231.796875,
"y": 269
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,36 @@
using System.Net;
using Elsa.Testing.Shared.Services;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.Runtime;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowDefinitionRefresh;
public class DynamicEndpointTests : AppComponentTest
{
private readonly IWorkflowDefinitionsRefresher _workflowDefinitionsRefresher;
public DynamicEndpointTests(App app) : base(app)
{
StaticValueHolder.Value = "first-value";
_workflowDefinitionsRefresher = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionsRefresher>();
}
[Fact]
public async Task ChangingEndpointValueThenRefresh_WorkflowShouldRespondToTheNewValue()
{
var client = WorkflowServer.CreateHttpWorkflowClient();
var firstResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "first-value"));
StaticValueHolder.Value = "second-value";
var _ = await _workflowDefinitionsRefresher.RefreshWorkflowDefinitionsAsync(
new Runtime.Requests.RefreshWorkflowDefinitionsRequest() { DefinitionIds = ["f69f061159adc3ae"] }, CancellationToken.None);
var secondResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "second-value"));
Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode);
}
}
@@ -0,0 +1,104 @@
{
"id": "69cb6deaa8e20599",
"definitionId": "f69f061159adc3ae",
"name": "Workflow 1",
"createdAt": "2024-07-04T13:41:33.1905134+00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "1bac532ee993dfdc",
"nodeId": "Workflow1:1bac532ee993dfdc",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"path": {
"typeName": "String",
"expression": {
"type": "JavaScript",
"value": "return getStaticValue() ?? \u0022first-value\u0022;"
}
},
"supportedMethods": {
"typeName": "String[]",
"expression": {
"type": "Literal",
"value": "[\u0022GET\u0022]"
}
},
"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": "25d3a6465094a796",
"nodeId": "Workflow1:1bac532ee993dfdc:25d3a6465094a796",
"name": "HttpEndpoint1",
"type": "Elsa.HttpEndpoint",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -260,
"y": 460
},
"size": {
"width": 176.390625,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,119 @@
using System.Net;
using Elsa.Common.Models;
using Elsa.Workflows.Activities;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
using Elsa.Workflows.ComponentTests.Materializers;
using Elsa.Workflows.ComponentTests.WorkflowProviders;
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime;
using Humanizer;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowDefinitionReload;
public class ReloadWorkflowTests : AppComponentTest
{
private readonly IActivityRegistry _activityRegistry;
private readonly TestWorkflowProvider _testWorkflowProvider;
private readonly IWorkflowBuilderFactory _workflowBuilderFactory;
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly IWorkflowDefinitionsReloader _workflowDefinitionsReloader;
public ReloadWorkflowTests(App app) : base(app)
{
_workflowDefinitionManager = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionManager>();
_workflowDefinitionsReloader = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionsReloader>();
_workflowBuilderFactory = Scope.ServiceProvider.GetRequiredService<IWorkflowBuilderFactory>();
_workflowDefinitionService = Scope.ServiceProvider.GetRequiredService<IWorkflowDefinitionService>();
_activityRegistry = Scope.ServiceProvider.GetRequiredService<IActivityRegistry>();
var workflowsProviders = Scope.ServiceProvider.GetRequiredService<IEnumerable<IWorkflowsProvider>>();
_testWorkflowProvider = (TestWorkflowProvider)workflowsProviders.First(x => x is TestWorkflowProvider);
}
[Fact]
public async Task Reloading_AfterRemovingTheWorkflow_ShouldMakeWorkflowReachableAgain()
{
var client = WorkflowServer.CreateHttpWorkflowClient();
await _workflowDefinitionManager.DeleteByDefinitionIdAsync("f68b09bc-2013-4617-b82f-d76b6819a624", CancellationToken.None);
var firstResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "reload-test"));
await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync();
var secondResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "reload-test"));
Assert.Equal(HttpStatusCode.NotFound, firstResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode);
}
[Fact]
public async Task Reloading_AfterUpdatingSourceProvider_ShouldRefreshCaches()
{
var definitionId = Guid.NewGuid().ToString();
var definitionVersionId1 = Guid.NewGuid().ToString();
var workflowV1 = await BuildWorkflowAsync(definitionId, definitionVersionId1, 1);
// Set up the initial workflow version.
_testWorkflowProvider.MaterializedWorkflows = [workflowV1];
await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync();
var definitionV1 = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Latest);
Assert.Equal(definitionVersionId1, definitionV1!.Workflow.Identity.Id);
// Simulate the workflow provider to have a new version available.
var definitionVersionId2 = Guid.NewGuid().ToString();
var workflowV2 = await BuildWorkflowAsync(definitionId, definitionVersionId2, 2);
_testWorkflowProvider.MaterializedWorkflows = [workflowV1, workflowV2];
// Reload the workflow definitions.
await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync();
// Assert that the workflow definition service finds the updated workflow version.
var definitionV2 = await _workflowDefinitionService.FindWorkflowGraphAsync(definitionId, VersionOptions.Latest);
Assert.Equal(definitionVersionId2, definitionV2!.Workflow.Identity.Id);
// Cleanup: Delete the workflow definition and its versions.
await _workflowDefinitionManager.DeleteByDefinitionIdAsync(definitionId, CancellationToken.None);
}
[Fact]
public async Task Reloading_AfterUpdatingSourceProvider_ShouldRefreshActivityRegistry()
{
var definitionId = Guid.NewGuid().ToString();
var definitionVersionId1 = Guid.NewGuid().ToString();
var workflowV1 = await BuildWorkflowAsync(definitionId, definitionVersionId1, 1);
// Set up the initial workflow version.
_testWorkflowProvider.MaterializedWorkflows = [workflowV1];
await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync();
var activityTypeName = workflowV1.Workflow.Name.Pascalize();
var activityV1 = _activityRegistry.Find(activityTypeName);
Assert.Equal(1, activityV1!.Version);
// Simulate the workflow provider to have a new version available.
var definitionVersionId2 = Guid.NewGuid().ToString();
var workflowV2 = await BuildWorkflowAsync(definitionId, definitionVersionId2, 2);
_testWorkflowProvider.MaterializedWorkflows = [workflowV1, workflowV2];
// Reload the workflow definitions.
await _workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync();
// Assert that the activity registry contains a new activity descriptor representing the new workflow version.
var activityV2 = _activityRegistry.Find(activityTypeName)!;
Assert.Equal(2, activityV2.Version);
// Cleanup: Delete the workflow definition and its versions.
await _workflowDefinitionManager.DeleteByDefinitionIdAsync(definitionId, CancellationToken.None);
}
private async Task<MaterializedWorkflow> BuildWorkflowAsync(string definitionId, string definitionVersionId, int version)
{
var builder = _workflowBuilderFactory.CreateBuilder();
builder.DefinitionId = definitionId;
builder.Id = definitionVersionId;
builder.Version = version;
builder.Name = definitionId;
builder.Root = new WriteLine($"Version {version}");
builder.WorkflowOptions.UsableAsActivity = true;
var workflow = await builder.BuildWorkflowAsync();
workflow.Name = definitionId;
return new MaterializedWorkflow(workflow, _testWorkflowProvider.Name, TestWorkflowMaterializer.MaterializerName);
}
}
@@ -0,0 +1,104 @@
{
"id": "c2e19917-d03e-41ed-9d19-ef5c1ddc9871",
"definitionId": "f68b09bc-2013-4617-b82f-d76b6819a624",
"name": "Workflow 1",
"createdAt": "2024-07-04T13:41:33.1905134+00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "f8019bae-c506-4467-812b-e403e7aad8ff",
"nodeId": "Workflow1:f8019bae-c506-4467-812b-e403e7aad8ff",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"path": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "reload-test"
}
},
"supportedMethods": {
"typeName": "String[]",
"expression": {
"type": "Literal",
"value": "[\u0022GET\u0022]"
}
},
"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": "cc196a5e-04f7-4794-b25c-1a0987632d8a",
"nodeId": "Workflow1:f8019bae-c506-4467-812b-e403e7aad8ff:cc196a5e-04f7-4794-b25c-1a0987632d8a",
"name": "HttpEndpoint1",
"type": "Elsa.HttpEndpoint",
"version": 1,
"customProperties": {
"canStartWorkflow": true,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -260,
"y": 460
},
"size": {
"width": 176.390625,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,32 @@
using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts;
using Elsa.Workflows.ComponentTests.Abstractions;
using Elsa.Workflows.ComponentTests.Fixtures;
namespace Elsa.Workflows.ComponentTests.Scenarios.WorkflowJsonStructures;
public class WorkflowJsonStructureTests(App app) : AppComponentTest(app)
{
private const string MainDefinitionId = "a417fadbbb7c417e";
private const string Sub1NodeId = "Workflow2:4d5af7585eece1d7:f19ae76011a020f3";
[Fact]
public async Task Workflow_ContainingWorkflowActivity_ShouldNotIncludeChildrenOfWorkflowActivity()
{
var client = WorkflowServer.CreateApiClient<IWorkflowDefinitionsApi>();
var workflowDefinition = (await client.GetByIdAsync(MainDefinitionId))!;
var root = workflowDefinition.Root;
var subActivity = root["activities"]![0]!;
Assert.Null(subActivity["root"]);
}
[Fact]
public async Task Requesting_Subgraph_Returns_ExpectedSubgraph()
{
var client = WorkflowServer.CreateApiClient<IWorkflowDefinitionsApi>();
var subgraph = (await client.GetSubgraphAsync(MainDefinitionId, Sub1NodeId))!;
Assert.Equal(Sub1NodeId, subgraph.Activity["nodeId"]!.ToString());
var writeLine = subgraph.Activity["root"]!["root"]!["activities"]![0]!;
Assert.Equal("Elsa.WriteLine", writeLine["type"]!.ToString());
}
}
@@ -0,0 +1,103 @@
{
"id": "a417fadbbb7c417e",
"definitionId": "ebe87d0f1d1924ea",
"name": "Main",
"createdAt": "2024-06-20T15:08:33.469865+00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "4d5af7585eece1d7",
"nodeId": "Workflow2:4d5af7585eece1d7",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"workflowDefinitionId": "497be45e7d647e8f",
"workflowDefinitionVersionId": "e91d58c7a7ca1861",
"latestAvailablePublishedVersion": 1,
"latestAvailablePublishedVersionId": "e91d58c7a7ca1861",
"id": "f19ae76011a020f3",
"nodeId": "Workflow2:4d5af7585eece1d7:f19ae76011a020f3",
"name": "Sub1",
"type": "Sub",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -235.796875,
"y": -74
},
"size": {
"width": 58.7421875,
"height": 50
}
}
}
},
{
"workflowDefinitionId": "497be45e7d647e8f",
"workflowDefinitionVersionId": "e91d58c7a7ca1861",
"latestAvailablePublishedVersion": 1,
"latestAvailablePublishedVersionId": "e91d58c7a7ca1861",
"id": "ad4d54104a443b5b",
"nodeId": "Workflow2:4d5af7585eece1d7:ad4d54104a443b5b",
"name": "Sub2",
"type": "Sub",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -101.796875,
"y": -74
},
"size": {
"width": 58.7421875,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "f19ae76011a020f3",
"port": "Done"
},
"target": {
"activity": "ad4d54104a443b5b",
"port": "In"
}
}
]
}
}
@@ -0,0 +1,69 @@
{
"id": "e91d58c7a7ca1861",
"definitionId": "497be45e7d647e8f",
"name": "Sub",
"createdAt": "2024-06-20T15:08:33.474045+00:00",
"version": 1,
"toolVersion": "3.3.0.0",
"variables": [],
"inputs": [],
"outputs": [],
"outcomes": [],
"customProperties": {
"Elsa:WorkflowContextProviderTypes": []
},
"isReadonly": false,
"isSystem": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "dc487ec63d2e8520",
"nodeId": "Workflow1:dc487ec63d2e8520",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"notFoundConnections": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Test"
}
},
"id": "f0f7dfe68eda4744",
"nodeId": "Workflow1:dc487ec63d2e8520:f0f7dfe68eda4744",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": -55.796875,
"y": -171
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,4 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
"parallelizeTestCollections": true
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,27 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Models;
using Xunit.Abstractions;
namespace Elsa.Activities.IntegrationTests;
public class SetNameTests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _serviceProvider;
public SetNameTests(ITestOutputHelper testOutputHelper)
{
_serviceProvider = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
}
[Fact(DisplayName = "SetName sets the workflow instance name.")]
public async Task Test1()
{
const string expectedName = "Foo";
var setName = new SetName(new Input<string>(expectedName));
var result = await _serviceProvider.RunActivityAsync(setName);
var actualName = result.WorkflowState.Name;
Assert.Equal(expectedName, actualName);
}
}
@@ -0,0 +1 @@
global using Xunit;
@@ -0,0 +1,25 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Xunit.Abstractions;
namespace Elsa.Activities.IntegrationTests;
public class WriteLineTests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _serviceProvider;
public WriteLineTests(ITestOutputHelper testOutputHelper)
{
_serviceProvider = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
}
[Fact(DisplayName = "WriteLine prints the expected line to the console.")]
public async Task Test1()
{
const string expectedLine = "Hello world!";
var writeLine = new WriteLine(expectedLine);
await _serviceProvider.RunActivityAsync(writeLine);
Assert.Equal(expectedLine, _capturingTextWriter.Lines.Single());
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Alterations\Elsa.Alterations.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Workflows\" />
</ItemGroup>
<ItemGroup>
<None Update="Workflows\version-1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Workflows\version-2.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,73 @@
using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Extensions;
using Elsa.Common.Models;
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows.Management;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Alterations.IntegrationTests;
/// <summary>
/// Contains tests for migration.
/// </summary>
public class MigrationTests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
private readonly IAlterationRunner _alterationRunner;
private readonly IWorkflowInstanceStore _workflowInstanceStore;
/// <summary>
/// Initializes a new instance of the <see cref="MigrationTests"/> class.
/// </summary>
public MigrationTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper)
.WithCapturingTextWriter(_capturingTextWriter)
.ConfigureElsa(elsa => elsa.UseAlterations())
.Build();
_alterationRunner = _services.GetRequiredService<IAlterationRunner>();
_workflowInstanceStore = _services.GetRequiredService<IWorkflowInstanceStore>();
}
/// <summary>
/// This method tests the migration from version 1 to version 2.
/// </summary>
[Fact(DisplayName = "Migrating from version 1 to 2 succeeds")]
public async Task Test1()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import workflows.
await _services.ImportWorkflowDefinitionAsync("Workflows/version-1.json");
await _services.ImportWorkflowDefinitionAsync("Workflows/version-2.json");
// Run version 1 workflow.
var workflowState = await _services.RunWorkflowUntilEndAsync("my-workflow", versionOptions: VersionOptions.SpecificVersion(1));
// Migrate to version 2.
var alterations = new IAlteration[]
{
new Migrate
{
TargetVersion = 2
}
};
var instanceIds = new[]
{
workflowState.Id
};
var migrationResult = await _alterationRunner.RunAsync(instanceIds, alterations);
// Assert success.
Assert.True(migrationResult.All(x => x.IsSuccessful));
// Assert that the workflow instance is now at version 2.
var workflowState2 = await _workflowInstanceStore.FindAsync(workflowState.Id);
Assert.Equal(2, workflowState2!.Version);
}
}
@@ -0,0 +1 @@
global using Xunit;
@@ -0,0 +1,41 @@
{
"id": "b760f121e7dc5dae",
"definitionId": "my-workflow",
"name": "Version 1",
"version": 1,
"toolVersion": "3.0.0.0",
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "52ae1d65f7896a9e",
"nodeId": "Workflow1:52ae1d65f7896a9e",
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Version 1"
}
},
"id": "3a1ab61a351fe232",
"nodeId": "Workflow1:52ae1d65f7896a9e:3a1ab61a351fe232",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"metadata": {
"designer": {
"position": {
"x": -121.796875,
"y": 272
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,41 @@
{
"id": "4aca2e3aa79475d3",
"definitionId": "my-workflow",
"name": "Version 2",
"version": 2,
"toolVersion": "3.0.0.0",
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "52ae1d65f7896a9e",
"nodeId": "Workflow1:52ae1d65f7896a9e",
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Version 2"
}
},
"id": "3a1ab61a351fe232",
"nodeId": "Workflow1:52ae1d65f7896a9e:3a1ab61a351fe232",
"name": "WriteLine1",
"type": "Elsa.WriteLine",
"metadata": {
"designer": {
"position": {
"x": -121.796875,
"y": 272
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": []
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.JavaScript\Elsa.JavaScript.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,61 @@
using System.Dynamic;
using System.Numerics;
using System.Text.Json;
using Elsa.Common.Converters;
using Elsa.Expressions.Models;
using Elsa.JavaScript.Contracts;
using Elsa.Testing.Shared;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.JavaScript.IntegrationTests;
public class JsonConverterTest(ITestOutputHelper testOutputHelper)
{
private readonly IServiceProvider _serviceProvider = new TestApplicationBuilder(testOutputHelper).Build();
[Fact(DisplayName = "JavaScript BigInt mapping to BigInteger serialization")]
public async Task Test1()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var script = @"return {
'BigNumber': BigInt('7239948466988781569')
}";
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var result = (await javaScriptEvaluator.EvaluateAsync(script, typeof(ExpandoObject), expressionExecutionContext))!;
var options = new JsonSerializerOptions()
{
Converters = { new BigIntegerJsonConverter() }
};
var serializedText = JsonSerializer.Serialize(result, options);
Assert.Equal("{\"BigNumber\":7239948466988781569}", serializedText);
}
[Fact(DisplayName = "JavaScript BigInt mapping to BigInteger serialization")]
public async Task Test2()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var script = @"return {
'BigNumber': BigInt('7239948466988781569')
}";
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var result = (await javaScriptEvaluator.EvaluateAsync(script, typeof(ExpandoObject), expressionExecutionContext))!;
var serializedText = JsonSerializer.Serialize(result);
Assert.Equal("{\"BigNumber\":{\"IsPowerOfTwo\":false,\"IsZero\":false,\"IsOne\":false,\"IsEven\":false,\"Sign\":1}}", serializedText);
}
[Fact(DisplayName = "BigIntegerJsonConverter Deserialize")]
public async Task Test3()
{
var options = new JsonSerializerOptions()
{
Converters = { new BigIntegerJsonConverter() }
};
BigInteger bigInteger = JsonSerializer.Deserialize<BigInteger>("7239948466988781569", options);
Assert.Equal(7239948466988781569, bigInteger);
}
}
@@ -0,0 +1,136 @@
using System.Text.Json;
using Elsa.Expressions.Models;
using Elsa.JavaScript.Contracts;
using Elsa.Testing.Shared;
using Elsa.Workflows.Memory;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.JavaScript.IntegrationTests;
public class JsonElementConverterTests(ITestOutputHelper testOutputHelper)
{
private readonly IServiceProvider _serviceProvider = new TestApplicationBuilder(testOutputHelper).Build();
[Fact(DisplayName = "JsonElement JsonObject can be passed to JavaScript")]
public async Task TestJsonObjectPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = "{\"name\": \"John\", \"age\": 30}";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
jsonVariable.Set(expressionExecutionContext, jsonElement);
var script = "getVariable('JsonVariable').age";
var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(int), expressionExecutionContext);
Assert.Equal(30, result);
}
[Fact(DisplayName = "JsonElement JsonArray can be passed to JavaScript")]
public async Task TestJsonArrayPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = "[1, 2, 3, 4, 5, 6]";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
jsonVariable.Set(expressionExecutionContext, jsonElement);
var script = "getVariable('JsonVariable')[3]";
var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(int), expressionExecutionContext);
Assert.Equal(4, result);
}
[Fact(DisplayName = "JsonElement string can be passed to JavaScript")]
public async Task TestStringPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = "\"I'm just a string\"";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
jsonVariable.Set(expressionExecutionContext, jsonElement);
var script = "getVariable('JsonVariable')";
var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(string), expressionExecutionContext);
Assert.Equal("I'm just a string", result);
}
[Fact(DisplayName = "JsonElement boolean can be passed to JavaScript")]
public async Task TestBooleanPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = "false";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
jsonVariable.Set(expressionExecutionContext, jsonElement);
var script = "getVariable('JsonVariable')";
var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(bool), expressionExecutionContext);
Assert.Equal(false, result);
}
[Fact(DisplayName = "JsonElement containing nested Json objects and arrays be passed to JavaScript")]
public async Task TestNestedJsonPassedAsJsonElement()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new());
var jsonVariable = new Variable<object>("JsonVariable", "");
var jsonString = @"
{
""name"": ""John Doe"",
""age"": 35,
""address"": {
""street"": ""123 Main St"",
""city"": ""Springfield"",
""zip"": ""12345"",
""coordinates"": { ""lat"": 40.7128, ""lng"": -74.006 }
},
""skills"": [
{ ""name"": ""Programming"", ""level"": ""Advanced"" },
{ ""name"": ""Writing"", ""level"": ""Intermediate"" }
],
""projects"": [
{
""title"": ""Project A"",
""status"": ""Completed"",
""team"": [""Alice"", ""Bob""]
},
{
""title"": ""Project B"",
""status"": ""In Progress"",
""team"": [""Charlie"", ""David"", ""Eve""]
}
],
""isEmployed"": true,
""contact"": { ""email"": ""john.doe@example.com"", ""phone"": ""555-1234"" }
}";
var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);
jsonVariable.Set(expressionExecutionContext, jsonElement);
var script = "getVariable('JsonVariable').projects[1].team[0]";
var result = await javaScriptEvaluator.EvaluateAsync(script, typeof(string), expressionExecutionContext);
Assert.Equal("Charlie", result);
var script2 = "getVariable('JsonVariable').isEmployed";
var result2 = await javaScriptEvaluator.EvaluateAsync(script2, typeof(bool), expressionExecutionContext);
Assert.Equal(true, result2);
var script3 = "getVariable('JsonVariable').skills[0].level";
var result3 = await javaScriptEvaluator.EvaluateAsync(script3, typeof(string), expressionExecutionContext);
Assert.Equal("Advanced", result3);
var script4 = "getVariable('JsonVariable').address.coordinates.lat";
var result4 = await javaScriptEvaluator.EvaluateAsync(script4, typeof(double), expressionExecutionContext);
Assert.Equal(40.7128, result4);
var script5 = "getVariable('JsonVariable').age";
var result5 = await javaScriptEvaluator.EvaluateAsync(script5, typeof(int), expressionExecutionContext);
Assert.Equal(35, result5);
}
}
@@ -0,0 +1,45 @@
using System.Text.Json;
using Elsa.Expressions.Models;
using Elsa.JavaScript.Contracts;
using Elsa.Testing.Shared;
using Elsa.Workflows.Memory;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.JavaScript.IntegrationTests;
public class ToJsonTests(ITestOutputHelper testOutputHelper)
{
private readonly IServiceProvider _serviceProvider = new TestApplicationBuilder(testOutputHelper).Build();
[Fact(DisplayName = "Serialize large unicode string using JavaScript's toJson function")]
public async Task Test1()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var unicodeString = UnicodeRangeGenerator.GenerateUnicodeString();
var script = $"toJson({{ text: '{unicodeString}' }})";
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var result = (string)(await javaScriptEvaluator.EvaluateAsync(script, typeof(string), expressionExecutionContext))!;
var serializedText = JsonDocument.Parse(result).RootElement.GetProperty("text").GetString();
Assert.Equal(unicodeString, serializedText);
}
[Fact(DisplayName = "Serialize large unicode string using JavaScript's toJson function from a workflow variable")]
public async Task Test2()
{
var javaScriptEvaluator = _serviceProvider.GetRequiredService<IJavaScriptEvaluator>();
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, new MemoryRegister());
var unicodeString = UnicodeRangeGenerator.GenerateUnicodeString();
var payloadVariable = new Variable<object>("Payload", null!);
var payload = new
{
Text = unicodeString
};
payloadVariable.Set(expressionExecutionContext, payload);
var script = "toJson(getPayload())";
var result = (string)(await javaScriptEvaluator.EvaluateAsync(script, typeof(string), expressionExecutionContext))!;
var serializedText = JsonDocument.Parse(result).RootElement.GetProperty("Text").GetString();
Assert.Equal(unicodeString, serializedText);
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared.Integration\Elsa.Testing.Shared.Integration.csproj" />
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Resilience\Elsa.Resilience.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,81 @@
using System.Net;
using Elsa.Extensions;
using Elsa.Http;
using Elsa.Resilience.Extensions;
using Elsa.Resilience.Models;
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Resilience.IntegrationTests;
public class FlowSendHttpRequestResilienceTests
{
private readonly IServiceProvider _services;
private readonly TestRetryAttemptRecorder _recorder = new();
public FlowSendHttpRequestResilienceTests(ITestOutputHelper output)
{
var handler = new SequentialStatusHandler([HttpStatusCode.TooManyRequests, HttpStatusCode.ServiceUnavailable, HttpStatusCode.OK]);
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Resilience:Strategies:0:$type"] = "HttpResilienceStrategy",
["Resilience:Strategies:0:Id"] = "test",
["Resilience:Strategies:0:DisplayName"] = "Test",
["Resilience:Strategies:0:MaxRetryAttempts"] = "5",
["Resilience:Strategies:0:Delay"] = "00:00:00.001"
})
.Build();
_services = new TestApplicationBuilder(output)
.ConfigureServices(s =>
{
s.AddSingleton<IConfiguration>(configuration);
})
.ConfigureElsa(module =>
{
module.UseHttp(http =>
{
http.HttpClientBuilder = builder => builder.ConfigurePrimaryHttpMessageHandler(() => handler);
});
module.UseResilience(resilience =>
{
resilience.WithRetryAttemptRecorder(_ => _recorder);
});
})
.Build();
}
[Fact(DisplayName = "FlowSendHttpRequest retries using selected resilience strategy")]
public async Task InvokesResilienceStrategy()
{
var activity = new FlowSendHttpRequest
{
Url = new(new Uri("http://localhost/test")),
Method = new("GET"),
ExpectedStatusCodes = new([200]),
CustomProperties =
{
["resilienceStrategy"] = new ResilienceStrategyConfig
{
Mode = ResilienceStrategyConfigMode.Identifier,
StrategyId = "test"
}
}
};
var result = await _services.RunActivityAsync(activity);
var statusCode = result.GetActivityOutput<int>(activity, nameof(SendHttpRequestBase.StatusCode));
Assert.Equal(200, statusCode);
Assert.Equal(2, _recorder.Attempts.Count);
Assert.Collection(_recorder.Attempts,
first => Assert.Equal(0, first.AttemptNumber),
second => Assert.Equal(1, second.AttemptNumber));
}
}
@@ -0,0 +1,19 @@
using System.Net;
using System.Net.Http.Headers;
namespace Elsa.Resilience.IntegrationTests;
public class SequentialStatusHandler(IEnumerable<HttpStatusCode> codes) : HttpMessageHandler
{
private readonly Queue<HttpStatusCode> _codes = new(codes);
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var code = _codes.Count > 0 ? _codes.Dequeue() : _codes.Last();
var response = new HttpResponseMessage(code)
{
Content = new StringContent(string.Empty)
};
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
return Task.FromResult(response);
}
}
@@ -0,0 +1,16 @@
using Elsa.Resilience.Entities;
using Elsa.Resilience.Models;
namespace Elsa.Resilience.IntegrationTests;
public class TestRetryAttemptRecorder : IRetryAttemptRecorder
{
public IList<RetryAttemptRecord> Attempts { get; } = new List<RetryAttemptRecord>();
public Task RecordAsync(RecordRetryAttemptsContext context)
{
foreach (var record in context.Attempts)
Attempts.Add(record);
return Task.CompletedTask;
}
}
@@ -0,0 +1 @@
global using Xunit;
@@ -0,0 +1,64 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.IntegrationTests.Activities.Workflows;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Activities;
public class BreakTests
{
private readonly IWorkflowRunner _workflowRunner;
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
public BreakTests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
_workflowRunner = _services.GetRequiredService<IWorkflowRunner>();
}
[Fact(DisplayName = "Break exits out of ForEach")]
public async Task Test1()
{
await _services.PopulateRegistriesAsync();
await _workflowRunner.RunAsync<BreakForEachWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Start", "C#", "End" }, lines);
}
[Fact(DisplayName = "Break exits out of immediate ForEach only")]
public async Task Test2()
{
await _services.PopulateRegistriesAsync();
await _workflowRunner.RunAsync<NestedForEachWithBreakWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "C#", "Classes", "Rust", "Classes", "Go", "Classes" }, lines);
}
[Fact(DisplayName = "Break exits out of For")]
public async Task Test3()
{
await _services.PopulateRegistriesAsync();
await _workflowRunner.RunAsync<BreakForWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Start", "0", "1", "End" }, lines);
}
[Fact(DisplayName = "Break exits out of While")]
public async Task Test4()
{
await _services.PopulateRegistriesAsync();
await _workflowRunner.RunAsync<BreakWhileWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Start", "1", "2", "End" }, lines);
}
[Fact(DisplayName = "Break removes the right bookmarks")]
public async Task Test5()
{
await _services.PopulateRegistriesAsync();
await _workflowRunner.RunAsync<BreakWhileWorkflow>();
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Start", "1", "2", "End" }, lines);
}
}
@@ -0,0 +1,38 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
class BreakForEachWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var items = new[] { "C#", "Rust", "Go" };
var currentItem = new Variable<string>("CurrentItem", "");
workflow.Root = new Sequence
{
Activities =
{
new WriteLine("Start"),
new ForEach<string>
{
Items = new(items),
CurrentValue = new (currentItem),
Body = new Sequence
{
Activities =
{
new If(context => currentItem.Get(context) == "Rust")
{
Then = new Break()
},
new WriteLine(currentItem)
}
}
},
new WriteLine("End"),
}
};
}
}
@@ -0,0 +1,36 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
class BreakForWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var currentValue = new Variable<int?>("CurrentValue", null);
workflow.Root = new Sequence
{
Activities =
{
new WriteLine("Start"),
new For(0, 3, 1)
{
CurrentValue = new (currentValue),
Body = new Sequence
{
Activities =
{
new If(context => currentValue.Get(context) == 2)
{
Then = new Break()
},
new WriteLine(context => currentValue.Get(context).ToString()),
}
}
},
new WriteLine("End"),
}
};
}
}
@@ -0,0 +1,51 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Workflows.IntegrationTests.Activities.Workflows;
public class BreakWhileForkWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
var currentValue = new Variable<int?>("CurrentValue", 0);
workflow.Root = new Sequence
{
Variables = { currentValue },
Activities =
{
While.True(new Fork
{
Branches =
{
new Sequence
{
Activities =
{
new SetVariable
{
Variable = currentValue,
Value = new (context => currentValue.Get(context) + 1)
},
new If(context => currentValue.Get(context) == 3)
{
Then = new Break()
}
}
},
new Sequence
{
Activities =
{
new WriteLine("Waiting for event..."),
new Event("Some event") { Id = "SomeEvent" },
new WriteLine("Resuming"),
}
}
}
}),
}
};
}
}

Some files were not shown because too many files have changed in this diff Show More