Merge branch 'develop/3.6.0' into feat/unit-test-coverage-switch
This commit is contained in:
commit
26079490cd
|
|
@ -9,174 +9,175 @@
|
|||
<ResilienceVersion>9.9.0</ResilienceVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Antlr4.Runtime.Standard" Version="4.13.1" PrivateAssets="All"/>
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations" Version="0.5.1"/>
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.MySql" Version="0.5.1"/>
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.PostgreSQL" Version="0.5.1"/>
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SqlServer" Version="0.5.1"/>
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite" Version="0.5.1"/>
|
||||
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="9.0.0"/>
|
||||
<PackageVersion Include="AutoMapper" Version="14.0.0" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Azure.Identity" Version="1.14.2"/>
|
||||
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.19.0"/>
|
||||
<PackageVersion Include="Azure.ResourceManager" Version="1.13.1"/>
|
||||
<PackageVersion Include="Azure.ResourceManager.AppContainers" Version="1.3.0"/>
|
||||
<PackageVersion Include="Azure.ResourceManager.Resources" Version="1.9.1"/>
|
||||
<PackageVersion Include="Azure.ResourceManager.ServiceBus" Version="1.1.0"/>
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.15.2"/>
|
||||
<PackageVersion Include="Bogus" Version="35.6.3"/>
|
||||
<PackageVersion Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Confluent.Kafka" Version="2.10.0"/>
|
||||
<PackageVersion Include="Confluent.SchemaRegistry.Serdes.Avro" Version="2.10.0"/>
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" PrivateAssets="All"/>
|
||||
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Cronos" Version="0.11.0"/>
|
||||
<PackageVersion Include="Dapper" Version="2.1.66"/>
|
||||
<PackageVersion Include="Datadog.Trace.Bundle" Version="3.16.0"/>
|
||||
<PackageVersion Include="DistributedLock" Version="2.6.0"/>
|
||||
<PackageVersion Include="DistributedLock.Core" Version="1.0.8"/>
|
||||
<PackageVersion Include="DistributedLock.FileSystem" Version="1.0.3"/>
|
||||
<PackageVersion Include="DistributedLock.Postgres" Version="1.3.0"/>
|
||||
<PackageVersion Include="DistributedLock.Redis" Version="1.0.3"/>
|
||||
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="9.0.6"/>
|
||||
<PackageVersion Include="Elsa.Studio" Version="$(ElsaStudioVersion)"/>
|
||||
<PackageVersion Include="Elsa.Studio.Agents" Version="$(ElsaStudioVersion)"/>
|
||||
<PackageVersion Include="Elsa.Studio.Core.BlazorWasm" Version="$(ElsaStudioVersion)"/>
|
||||
<PackageVersion Include="Elsa.Studio.Login.BlazorWasm" Version="$(ElsaStudioVersion)"/>
|
||||
<PackageVersion Include="FastEndpoints" Version="7.0.1"/>
|
||||
<PackageVersion Include="FastEndpoints.Security" Version="7.0.1"/>
|
||||
<PackageVersion Include="FastEndpoints.Swagger" Version="7.0.1"/>
|
||||
<PackageVersion Include="FluentMigrator" Version="7.1.0"/>
|
||||
<PackageVersion Include="FluentMigrator.Runner" Version="7.1.0"/>
|
||||
<PackageVersion Include="FluentStorage" Version="5.6.0"/>
|
||||
<PackageVersion Include="FluentStorage.Azure.Blobs" Version="5.3.0"/>
|
||||
<PackageVersion Include="Fluid.Core" Version="2.25.0"/>
|
||||
<PackageVersion Include="Fody" Version="6.9.2" PrivateAssets="All"/>
|
||||
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Google.Protobuf" Version="3.31.1"/>
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.71.0"/>
|
||||
<PackageVersion Include="Grpc.Tools" Version="2.72.0" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Hangfire" Version="1.8.20"/>
|
||||
<PackageVersion Include="Hangfire.MemoryStorage" Version="1.8.1.1"/>
|
||||
<PackageVersion Include="Hangfire.PostgreSql" Version="1.20.12"/>
|
||||
<PackageVersion Include="Hangfire.Storage.SQLite" Version="0.4.2"/>
|
||||
<PackageVersion Include="Humanizer.Core" Version="2.14.1"/>
|
||||
<PackageVersion Include="IronCompress" Version="1.6.3"/>
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0"/>
|
||||
<PackageVersion Include="Jint" Version="4.4.1"/>
|
||||
<PackageVersion Include="LinqKit.Core" Version="1.2.8"/>
|
||||
<PackageVersion Include="MailKit" Version="4.12.1"/>
|
||||
<PackageVersion Include="MassTransit" Version="8.4.1"/>
|
||||
<PackageVersion Include="MassTransit.Azure.ServiceBus.Core" Version="8.4.1"/>
|
||||
<PackageVersion Include="MassTransit.Extensions.DependencyInjection" Version="7.3.1"/>
|
||||
<PackageVersion Include="MassTransit.RabbitMQ" Version="8.4.1"/>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0"/>
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.2"/>
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6"/>
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(ResilienceVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Resilience" Version="$(ResilienceVersion)"/>
|
||||
<PackageVersion Include="Microsoft.Identity.Client" Version="4.72.1"/>
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Moq" Version="4.20.72"/>
|
||||
<PackageVersion Include="MongoDB.Driver" Version="3.4.0"/>
|
||||
<PackageVersion Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0"/>
|
||||
<PackageVersion Include="MongoDB.Driver.Extensions" Version="2.0.2"/>
|
||||
<PackageVersion Include="MySql.Data" Version="9.3.0"/>
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageVersion Include="Npgsql" Version="9.0.3"/>
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||
<PackageVersion Include="NSubstitute" Version="5.3.0"/>
|
||||
<PackageVersion Include="NuGet.Packaging" Version="6.14.0"/>
|
||||
<PackageVersion Include="NuGet.Protocol" Version="6.14.0"/>
|
||||
<PackageVersion Include="Nuke.Components" Version="9.0.4"/>
|
||||
<PackageVersion Include="Open.Linq.AsyncExtensions" Version="1.2.0"/>
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.12.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.AutoInstrumentation" Version="1.11.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.12.0-beta.1"/>
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.12.0-beta.1"/>
|
||||
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="9.23.80"/>
|
||||
<PackageVersion Include="Polly" Version="8.6.2"/>
|
||||
<PackageVersion Include="Polly.Extensions" Version="8.6.2"/>
|
||||
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0"/>
|
||||
<PackageVersion Include="PolySharp" Version="1.15.0" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0"/>
|
||||
<PackageVersion Include="Proto.Actor" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Cluster" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Cluster.CodeGen" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Cluster.AzureContainerApps" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Cluster.Kubernetes" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Cluster.TestProvider" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.OpenTelemetry" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Persistence" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Persistence.Sqlite" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Persistence.SqlServer" Version="1.7.0"/>
|
||||
<PackageVersion Include="Proto.Remote" Version="1.7.0"/>
|
||||
<PackageVersion Include="pythonnet" Version="3.1.0-preview2024-09-06"/>
|
||||
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.14.0"/>
|
||||
<PackageVersion Include="Quartz.Serialization.Json" Version="3.14.0"/>
|
||||
<PackageVersion Include="Refit" Version="8.0.0"/>
|
||||
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0"/>
|
||||
<PackageVersion Include="Scrutor" Version="6.1.0"/>
|
||||
<PackageVersion Include="Serilog" Version="4.3.0"/>
|
||||
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.2"/>
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0"/>
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0"/>
|
||||
<PackageVersion Include="ShortGuid" Version="2.0.1"/>
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.8.37"/>
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1"/>
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0"/>
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3"/>
|
||||
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.6.7"/>
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4"/>
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1"/>
|
||||
<PackageVersion Include="System.Formats.Asn1" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="System.Text.Json" Version="$(MicrosoftVersion)"/>
|
||||
<PackageVersion Include="Testcontainers" Version="4.6.0"/>
|
||||
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.6.0"/>
|
||||
<PackageVersion Include="Testcontainers.RabbitMq" Version="4.6.0"/>
|
||||
<PackageVersion Include="Testcontainers.Redis" Version="4.6.0"/>
|
||||
<PackageVersion Include="ThrottleDebounce" Version="2.0.1"/>
|
||||
<PackageVersion Include="WebhooksCore" Version="0.0.1"/>
|
||||
<PackageVersion Include="xunit" Version="2.9.3"/>
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3"/>
|
||||
<PackageVersion Include="xunit.extensibility.core" Version="2.9.3"/>
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" PrivateAssets="All"/>
|
||||
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0"/>
|
||||
<PackageVersion Include="Antlr4.Runtime.Standard" Version="4.13.1" PrivateAssets="All" />
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations" Version="0.5.1" />
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.MySql" Version="0.5.1" />
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.PostgreSQL" Version="0.5.1" />
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SqlServer" Version="0.5.1" />
|
||||
<PackageVersion Include="AppAny.Quartz.EntityFrameworkCore.Migrations.SQLite" Version="0.5.1" />
|
||||
<PackageVersion Include="AspNetCore.Authentication.ApiKey" Version="9.0.0" />
|
||||
<PackageVersion Include="AutoMapper" Version="14.0.0" PrivateAssets="All" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.14.2" />
|
||||
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.19.0" />
|
||||
<PackageVersion Include="Azure.ResourceManager" Version="1.13.1" />
|
||||
<PackageVersion Include="Azure.ResourceManager.AppContainers" Version="1.3.0" />
|
||||
<PackageVersion Include="Azure.ResourceManager.Resources" Version="1.9.1" />
|
||||
<PackageVersion Include="Azure.ResourceManager.ServiceBus" Version="1.1.0" />
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.15.2" />
|
||||
<PackageVersion Include="Bogus" Version="35.6.3" />
|
||||
<PackageVersion Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All" />
|
||||
<PackageVersion Include="Confluent.Kafka" Version="2.10.0" />
|
||||
<PackageVersion Include="Confluent.SchemaRegistry.Serdes.Avro" Version="2.10.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" PrivateAssets="All" />
|
||||
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" PrivateAssets="All" />
|
||||
<PackageVersion Include="Cronos" Version="0.11.0" />
|
||||
<PackageVersion Include="Dapper" Version="2.1.66" />
|
||||
<PackageVersion Include="Datadog.Trace.Bundle" Version="3.16.0" />
|
||||
<PackageVersion Include="DistributedLock" Version="2.6.0" />
|
||||
<PackageVersion Include="DistributedLock.Core" Version="1.0.8" />
|
||||
<PackageVersion Include="DistributedLock.FileSystem" Version="1.0.3" />
|
||||
<PackageVersion Include="DistributedLock.Postgres" Version="1.3.0" />
|
||||
<PackageVersion Include="DistributedLock.Redis" Version="1.0.3" />
|
||||
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="9.0.6" />
|
||||
<PackageVersion Include="Elsa.Studio" Version="$(ElsaStudioVersion)" />
|
||||
<PackageVersion Include="Elsa.Studio.Agents" Version="$(ElsaStudioVersion)" />
|
||||
<PackageVersion Include="Elsa.Studio.Core.BlazorWasm" Version="$(ElsaStudioVersion)" />
|
||||
<PackageVersion Include="Elsa.Studio.Login.BlazorWasm" Version="$(ElsaStudioVersion)" />
|
||||
<PackageVersion Include="FastEndpoints" Version="7.0.1" />
|
||||
<PackageVersion Include="FastEndpoints.Security" Version="7.0.1" />
|
||||
<PackageVersion Include="FastEndpoints.Swagger" Version="7.0.1" />
|
||||
<PackageVersion Include="FluentMigrator" Version="7.1.0" />
|
||||
<PackageVersion Include="FluentMigrator.Runner" Version="7.1.0" />
|
||||
<PackageVersion Include="FluentStorage" Version="5.6.0" />
|
||||
<PackageVersion Include="FluentStorage.Azure.Blobs" Version="5.3.0" />
|
||||
<PackageVersion Include="Fluid.Core" Version="2.25.0" />
|
||||
<PackageVersion Include="Fody" Version="6.9.2" PrivateAssets="All" />
|
||||
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="All" />
|
||||
<PackageVersion Include="Google.Protobuf" Version="3.31.1" />
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
<PackageVersion Include="Grpc.Tools" Version="2.72.0" PrivateAssets="All" />
|
||||
<PackageVersion Include="Hangfire" Version="1.8.20" />
|
||||
<PackageVersion Include="Hangfire.MemoryStorage" Version="1.8.1.1" />
|
||||
<PackageVersion Include="Hangfire.PostgreSql" Version="1.20.12" />
|
||||
<PackageVersion Include="Hangfire.Storage.SQLite" Version="0.4.2" />
|
||||
<PackageVersion Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageVersion Include="IronCompress" Version="1.6.3" />
|
||||
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||
<PackageVersion Include="Jint" Version="4.4.1" />
|
||||
<PackageVersion Include="LinqKit.Core" Version="1.2.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.12.1" />
|
||||
<PackageVersion Include="MassTransit" Version="8.4.1" />
|
||||
<PackageVersion Include="MassTransit.Azure.ServiceBus.Core" Version="8.4.1" />
|
||||
<PackageVersion Include="MassTransit.Extensions.DependencyInjection" Version="7.3.1" />
|
||||
<PackageVersion Include="MassTransit.RabbitMQ" Version="8.4.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(ResilienceVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Resilience" Version="$(ResilienceVersion)" />
|
||||
<PackageVersion Include="Microsoft.Identity.Client" Version="4.72.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
|
||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||
<PackageVersion Include="MongoDB.Driver" Version="3.4.0" />
|
||||
<PackageVersion Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0" />
|
||||
<PackageVersion Include="MongoDB.Driver.Extensions" Version="2.0.2" />
|
||||
<PackageVersion Include="MySql.Data" Version="9.3.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Npgsql" Version="9.0.3" />
|
||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageVersion Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageVersion Include="NuGet.Packaging" Version="6.14.0" />
|
||||
<PackageVersion Include="NuGet.Protocol" Version="6.14.0" />
|
||||
<PackageVersion Include="Nuke.Components" Version="9.0.4" />
|
||||
<PackageVersion Include="Open.Linq.AsyncExtensions" Version="1.2.0" />
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.AutoInstrumentation" Version="1.11.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.12.0-beta.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.12.0-beta.1" />
|
||||
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="9.23.80" />
|
||||
<PackageVersion Include="Polly" Version="8.6.2" />
|
||||
<PackageVersion Include="Polly.Extensions" Version="8.6.2" />
|
||||
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
|
||||
<PackageVersion Include="PolySharp" Version="1.15.0" PrivateAssets="All" />
|
||||
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
|
||||
<PackageVersion Include="Proto.Actor" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Cluster" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Cluster.CodeGen" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Cluster.AzureContainerApps" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Cluster.Kubernetes" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Cluster.TestProvider" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.OpenTelemetry" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Persistence" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Persistence.Sqlite" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Persistence.SqlServer" Version="1.7.0" />
|
||||
<PackageVersion Include="Proto.Remote" Version="1.7.0" />
|
||||
<PackageVersion Include="pythonnet" Version="3.1.0-preview2024-09-06" />
|
||||
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||
<PackageVersion Include="Quartz.Serialization.Json" Version="3.14.0" />
|
||||
<PackageVersion Include="Refit" Version="8.0.0" />
|
||||
<PackageVersion Include="Refit.HttpClientFactory" Version="8.0.0" />
|
||||
<PackageVersion Include="Scrutor" Version="6.1.0" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageVersion Include="ShortGuid" Version="2.0.1" />
|
||||
<PackageVersion Include="StackExchange.Redis" Version="2.8.37" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.6.7" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="System.Formats.Asn1" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="System.Text.Json" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Testcontainers" Version="4.6.0" />
|
||||
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.6.0" />
|
||||
<PackageVersion Include="Testcontainers.RabbitMq" Version="4.6.0" />
|
||||
<PackageVersion Include="Testcontainers.Redis" Version="4.6.0" />
|
||||
<PackageVersion Include="ThrottleDebounce" Version="2.0.1" />
|
||||
<PackageVersion Include="WebhooksCore" Version="0.0.1" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageVersion Include="xunit.assert" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.extensibility.core" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" PrivateAssets="All" />
|
||||
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
14
Elsa.sln
14
Elsa.sln
|
|
@ -311,6 +311,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qa", "qa", "{0478E6EA-DCB2-
|
|||
doc\qa\test-guidelines.md = doc\qa\test-guidelines.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Workflows.Management.UnitTests", "test\unit\Elsa.Workflows.Management.UnitTests\Elsa.Workflows.Management.UnitTests.csproj", "{20C75446-CAE0-48D1-85D9-459541C4D8B6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Expressions.UnitTests", "test\unit\Elsa.Expressions.UnitTests\Elsa.Expressions.UnitTests.csproj", "{67E1F0CC-C436-4D71-AD2C-AE42E3AA8A0B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -619,6 +623,14 @@ Global
|
|||
{2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{20C75446-CAE0-48D1-85D9-459541C4D8B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{20C75446-CAE0-48D1-85D9-459541C4D8B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{20C75446-CAE0-48D1-85D9-459541C4D8B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{20C75446-CAE0-48D1-85D9-459541C4D8B6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{67E1F0CC-C436-4D71-AD2C-AE42E3AA8A0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{67E1F0CC-C436-4D71-AD2C-AE42E3AA8A0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{67E1F0CC-C436-4D71-AD2C-AE42E3AA8A0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{67E1F0CC-C436-4D71-AD2C-AE42E3AA8A0B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -714,6 +726,8 @@ Global
|
|||
{DC9CCAD0-7363-4691-B964-FF5B3AEA3F95} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{2DA466EB-CBF0-46EC-8B86-45CAF2B68BBA} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{0478E6EA-DCB2-4667-ADC2-37C62C9C2574} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6}
|
||||
{20C75446-CAE0-48D1-85D9-459541C4D8B6} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
{67E1F0CC-C436-4D71-AD2C-AE42E3AA8A0B} = {18453B51-25EB-4317-A4B3-B10518252E92}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Each test layer has distinct goals and clear boundaries — see [**Which parts o
|
|||
|
||||
## Quick Start for Contributors
|
||||
|
||||
**Before you writ e a test:**
|
||||
**Before you write a test:**
|
||||
1. ✅ Understand what you're testing (see [**Which parts of Elsa to test**](#which-parts-of-elsa-to-test-and-which-test-types-to-use))
|
||||
2. ✅ Choose the right test layer (unit vs integration vs component)
|
||||
3. ✅ Use existing helpers (don't reinvent - see [**Test Helpers Reference**](#test-helpers-reference-quick-lookup))
|
||||
|
|
@ -68,6 +68,24 @@ Each test layer has distinct goals and clear boundaries — see [**Which parts o
|
|||
|
||||
---
|
||||
|
||||
## Test Project Organization
|
||||
|
||||
Tests are organized by module and test type:
|
||||
|
||||
- **`test/unit/Elsa.Activities.UnitTests`** - Activity unit tests
|
||||
- **`test/unit/Elsa.Expressions.UnitTests`** - Expression evaluation unit tests
|
||||
- **`test/unit/Elsa.Workflows.Core.UnitTests`** - Core workflow unit tests (ActivityExecutionContext extensions, etc.)
|
||||
- **`test/unit/Elsa.Workflows.Management.UnitTests`** - Workflow management unit tests
|
||||
- **`test/integration/Elsa.Workflows.IntegrationTests`** - Workflow integration tests
|
||||
- **`test/component/Elsa.Workflows.ComponentTests`** - End-to-end component tests
|
||||
|
||||
**Shared test infrastructure:**
|
||||
- **`src/common/Elsa.Testing.Shared`** - Shared test helpers (`ActivityTestFixture`, fake strategies, extensions)
|
||||
- **`src/common/Elsa.Testing.Shared.Integration`** - Integration test helpers (`RunActivityExtensions`, `PopulateRegistriesAsync`)
|
||||
- **`src/common/Elsa.Testing.Shared.Component`** - Component test helpers (event handlers, workflow events)
|
||||
|
||||
---
|
||||
|
||||
## Which parts of Elsa to test, and which test types to use
|
||||
|
||||
This section maps Elsa aspects to the exact kinds of tests you should write, with examples and code patterns referencing repository conventions.
|
||||
|
|
@ -76,8 +94,9 @@ This section maps Elsa aspects to the exact kinds of tests you should write, wit
|
|||
|
||||
#### **Unit tests:**
|
||||
- Test the activity class logic only (no persistence, no scheduler). Cover configuration permutations and boundary inputs.
|
||||
- Use [`ActivityTestHelper`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs), `ExecuteActivityAsync` method to run the activity and obtain an [`ActivityExecutionContext`](../../src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs) for assertions.
|
||||
- In case the activity sets outputs, assert using `context.GetExecutionOutput()`([extension method](../../src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs)), otherwise, use `context.Get()`.
|
||||
- Use [`ActivityTestFixture`](../../src/common/Elsa.Testing.Shared/ActivityTestFixture.cs) to configure and execute the activity and obtain an [`ActivityExecutionContext`](../../src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs) for assertions.
|
||||
- Use fluent methods: `ConfigureServices()` to add custom services, `ConfigureContext()` to set up context state
|
||||
- Assert using `context.GetActivityOutput()`([extension method](../../src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs)) for outputs, or `context.Get()` for variables.
|
||||
|
||||
**Example (does not set output):**
|
||||
```csharp
|
||||
|
|
@ -90,7 +109,8 @@ public async Task Should_Set_Variable_Integer()
|
|||
var setVariable = new SetVariable<int>(variable, new Input<int>(expected));
|
||||
|
||||
// Act
|
||||
var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable);
|
||||
var fixture = new ActivityTestFixture(setVariable);
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
var result = context.Get(variable);
|
||||
|
|
@ -98,39 +118,56 @@ public async Task Should_Set_Variable_Integer()
|
|||
}
|
||||
```
|
||||
|
||||
**Example (sets output):**
|
||||
**Example (sets output using fluent configuration):**
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Should_Send_GET_Request_And_Handle_Success_Response()
|
||||
public async Task Should_Send_Get_Request_And_Handle_Success_Response()
|
||||
{
|
||||
// Arrange
|
||||
var expectedUrl = new Uri("https://example.com");
|
||||
var sendHttpRequest = new SendHttpRequest
|
||||
{
|
||||
// Arrange
|
||||
// Extra arrange steps omitted for brevity
|
||||
var sendHttpRequest = new SendHttpRequest
|
||||
{
|
||||
Url = new Input<Uri?>(expectedUrl),
|
||||
Method = new Input<string>("GET"),
|
||||
ExpectedStatusCodes = new List<HttpStatusCodeCase>()
|
||||
};
|
||||
Url = new Input<Uri?>(expectedUrl),
|
||||
Method = new Input<string>("GET")
|
||||
};
|
||||
|
||||
// Act
|
||||
var context = await ActivityTestHelper.ExecuteActivityAsync(sendHttpRequest, services =>
|
||||
{
|
||||
// http configuration omitted for brevity
|
||||
});
|
||||
// Act
|
||||
var fixture = new ActivityTestFixture(sendHttpRequest)
|
||||
.WithHttpServices(responseHandler); // HTTP-specific extension
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
var statusCodeOutput = context.GetExecutionOutput(_ => sendHttpRequest.StatusCode);
|
||||
Assert.Equal(200, statusCodeOutput);
|
||||
}
|
||||
// Assert
|
||||
var statusCodeOutput = context.GetActivityOutput(_ => sendHttpRequest.StatusCode);
|
||||
Assert.Equal(200, statusCodeOutput);
|
||||
}
|
||||
```
|
||||
|
||||
**Example (checking scheduled activities):**
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Should_Schedule_Child_Activity()
|
||||
{
|
||||
// Arrange
|
||||
var childActivity = new WriteLine("Hello");
|
||||
var parentActivity = new Sequence { Activities = [childActivity] };
|
||||
|
||||
// Act
|
||||
var fixture = new ActivityTestFixture(parentActivity);
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(childActivity));
|
||||
}
|
||||
```
|
||||
|
||||
#### **Integration tests:**
|
||||
- Place the activity inside a minimal workflow definition and run via [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs). Assert outputs/variables and that the activity integrates correctly with preceding/following activities.
|
||||
- If activity creates bookmarks or relies on scheduler semantics, integration tests should resume bookmarks via the engine APIs to validate resumption.
|
||||
|
||||
Pattern note: [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) returns a [`RunWorkflowResult`](../../src/modules/Elsa.Workflows.Core/Models/RunWorkflowResult.cs) (or equivalent) containing the [`WorkflowInstance`](../../src/modules/Elsa.Workflows.Management/Entities/WorkflowInstance.cs) and output variables when run to completion.
|
||||
Pattern note: [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) returns a [`RunWorkflowResult`](../../src/modules/Elsa.Workflows.Core/Models/RunWorkflowResult.cs) (or equivalent) containing the [`WorkflowInstance`](../../src/modules/Elsa.Workflows.Management/Entities/WorkflowInstance.cs) and output variables when run to completion.
|
||||
Use returned state for deterministic assertions where possible.
|
||||
|
||||
---
|
||||
|
||||
### Workflow execution (invoker, middleware, bookmarks)
|
||||
|
||||
|
|
@ -167,14 +204,38 @@ Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowInstance.Status);
|
|||
|
||||
## Test Helpers Reference (Quick Lookup)
|
||||
|
||||
| Helper | Purpose | Use When |
|
||||
|-------------------------------------------|--------------------------------------------|-------------------------------------------------------------------------|
|
||||
| `TestApplicationBuilder` | Build test service provider | All tests as entry point, <br/>except activities unit tests (see below) |
|
||||
| `ActivityTestHelper.ExecuteActivityAsync` | Run single activity, with isolated context | Unit testing activities |
|
||||
| `IWorkflowRunner.RunAsync` | Execute workflow in-process | Integration / Component tests |
|
||||
| `PopulateRegistriesAsync` | Register types for JSON deserialization. | Loading JSON workflows. <br/>Integration tests only |
|
||||
| `IWorkflowInstanceStore` | Query persisted instances | Component tests (persistence) |
|
||||
| `RunWorkflowUntilEndAsync` | Drive workflow to completion. | Complex resumption scenarios. <br/>Integration tests only |
|
||||
| Helper | Purpose | Use When |
|
||||
|-------------------------------------------------|--------------------------------------------------------------|-------------------------------------------------------------------------|
|
||||
| `TestApplicationBuilder` | Build test service provider | All tests as entry point, <br/>except activities unit tests (see below) |
|
||||
| `ActivityTestFixture` | Configure and run single activity in isolation | Unit testing activities |
|
||||
| `ActivityTestFixture.ConfigureServices` | Fluent API to add services to fixture | Activity unit tests requiring custom services |
|
||||
| `ActivityTestFixture.ConfigureContext` | Fluent API to configure execution context before execution | Setting up test-specific context state |
|
||||
| `ActivityTestFixture.BuildAsync` | Build context without executing activity | When you need context setup without execution |
|
||||
| `ActivityTestFixture.ExecuteAsync` | Execute activity and return context | Standard activity unit test execution |
|
||||
| `context.GetActivityOutput` | Get output from activity using expression selector | Asserting activity outputs in unit tests |
|
||||
| `context.HasScheduledActivity` | Check if activity is scheduled | Verifying scheduling behavior in unit tests |
|
||||
| `IWorkflowRunner.RunAsync` | Execute workflow in-process | Integration / Component tests |
|
||||
| `RunActivityExtensions.RunActivityAsync` | Run single activity as workflow | Integration tests for single activities |
|
||||
| `PopulateRegistriesAsync` | Register types for JSON deserialization | Loading JSON workflows. <br/>Integration tests only |
|
||||
| `IWorkflowInstanceStore` | Query persisted instances | Component tests (persistence) |
|
||||
| `AsyncWorkflowRunner.RunAndAwaitWorkflowCompletionAsync` | Drive workflow to completion with activity tracking | Complex async scenarios. <br/>Component tests only |
|
||||
| `FakeActivityExecutionContextSchedulerStrategy` | Fake scheduler for unit tests | Automatically configured in `ActivityTestFixture` |
|
||||
| `FakeWorkflowExecutionContextSchedulerStrategy` | Fake workflow scheduler for unit tests | Automatically configured in `ActivityTestFixture` |
|
||||
|
||||
---
|
||||
|
||||
## Scheduler Strategies for Testing
|
||||
|
||||
When testing activities in isolation, Elsa provides fake scheduler strategy implementations that bypass the production scheduling logic:
|
||||
|
||||
### Available Strategies
|
||||
|
||||
- **`FakeActivityExecutionContextSchedulerStrategy`** ([source](../../src/common/Elsa.Testing.Shared/FakeActivityExecutionContextSchedulerStrategy.cs)) - Schedules activities immediately within the activity execution context for deterministic testing
|
||||
- **`FakeWorkflowExecutionContextSchedulerStrategy`** ([source](../../src/common/Elsa.Testing.Shared/FakeWorkflowExecutionContextSchedulerStrategy.cs)) - Schedules activities immediately at the workflow level for deterministic testing
|
||||
|
||||
These strategies are automatically configured when using [`ActivityTestFixture`](../../src/common/Elsa.Testing.Shared/ActivityTestFixture.cs) and ensure that scheduled activities execute synchronously in tests, making assertions deterministic.
|
||||
|
||||
**Note:** You do not need to manually configure these strategies - `ActivityTestFixture` handles this for you.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -198,9 +259,9 @@ When in doubt, add the minimal unit tests plus one integration test that reprodu
|
|||
|
||||
## Deterministic patterns to avoid flaky tests
|
||||
|
||||
1. **For activities unit tests, prefer returned state from [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs).** Always inspect on the returned context — it is deterministic for synchronous workflows.
|
||||
1. **For activity unit tests, prefer returned state from [`ActivityTestFixture.ExecuteAsync`](../../src/common/Elsa.Testing.Shared/ActivityTestFixture.cs).** Always inspect on the returned context — it is deterministic for synchronous workflows.
|
||||
2. **Resume bookmarks explicitly.** Do not wait for external schedulers — call the engine's resume/trigger APIs in your test to continue execution.
|
||||
3. **For integration tests, Locate instances deterministically.** Use an instance id returned by [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) or attach a `CorrelationId` test variable and query [`IWorkflowInstanceStore.FindByCorrelationIdAsync(...)`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). Avoid using "latest" queries.
|
||||
3. **For integration tests, locate instances deterministically.** Use an instance id returned by [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) or attach a `CorrelationId` test variable and query [`IWorkflowInstanceStore.FindByCorrelationIdAsync(...)`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs). Avoid using "latest" queries.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -210,7 +271,7 @@ When in doubt, add the minimal unit tests plus one integration test that reprodu
|
|||
|
||||
---
|
||||
|
||||
## Practical test recipes & snippets (copy/paste-ready)
|
||||
## Practical test recipes & snippets
|
||||
|
||||
### Unit test (activity) — pattern
|
||||
|
||||
|
|
@ -221,7 +282,8 @@ public async Task MyActivity_Test()
|
|||
var activity = new ActivityToTest();
|
||||
|
||||
// Act
|
||||
var context = await ActivityTestHelper.ExecuteActivityAsync(activity);
|
||||
var fixture = new ActivityTestFixture(activity);
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// assert behavior of activity in isolation
|
||||
}
|
||||
|
|
@ -255,10 +317,35 @@ public async Task Workflow_Persists_Instance_And_Journal()
|
|||
.Build();
|
||||
|
||||
var runner = sp.GetRequiredService<AsyncWorkflowRunner>();
|
||||
var result = await runner.RunAndAwaitWorkflowCompletionAsync(WorkflowDefinitionHandle.ByDefinitionId(someDefinitionId, VersionOptions.Published));
|
||||
var result = await runner.RunAndAwaitWorkflowCompletionAsync(
|
||||
WorkflowDefinitionHandle.ByDefinitionId(someDefinitionId, VersionOptions.Published)
|
||||
);
|
||||
result.WorkflowExecutionContext.Status.Should().Be(WorkflowStatus.Finished);
|
||||
}
|
||||
```
|
||||
|
||||
### Component test with async workflow execution
|
||||
|
||||
For testing workflows that complete asynchronously (e.g., with timers, external triggers), use [`AsyncWorkflowRunner`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Services/AsyncWorkflowRunner.cs):
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Workflow_Completes_Asynchronously()
|
||||
{
|
||||
var sp = new TestApplicationBuilder(testOutput).Build();
|
||||
var runner = sp.GetRequiredService<AsyncWorkflowRunner>();
|
||||
|
||||
var result = await runner.RunAndAwaitWorkflowCompletionAsync(
|
||||
WorkflowDefinitionHandle.ByDefinitionId(workflowId, VersionOptions.Published)
|
||||
);
|
||||
|
||||
result.WorkflowExecutionContext.Status.Should().Be(WorkflowStatus.Finished);
|
||||
result.ActivityExecutionRecords.Should().HaveCount(expectedCount);
|
||||
}
|
||||
```
|
||||
|
||||
`AsyncWorkflowRunner` tracks activity execution records and awaits workflow completion signals, making it ideal for testing asynchronous workflow behavior deterministically.
|
||||
|
||||
---
|
||||
|
||||
## FAQ (quick pointers)
|
||||
|
|
@ -271,7 +358,7 @@ Leave the definitions next to the tests that use them.
|
|||
|
||||
**Q: Which helper should I use to run a workflow?**
|
||||
|
||||
A: Prefer [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) for in-process deterministic runs. For activities use `ExecuteActivityAsync` via [`ActivityTestHelper`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs).
|
||||
A: Prefer [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) for in-process deterministic runs. For activities use [`ActivityTestFixture`](../../src/common/Elsa.Testing.Shared/ActivityTestFixture.cs).
|
||||
|
||||
**Q: How do I check persisted journal entries?**
|
||||
|
||||
|
|
@ -279,7 +366,7 @@ A: Query [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/
|
|||
|
||||
**Q: Do I need a new helper to wait for workflow completion?**
|
||||
|
||||
A: No. The repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) for integration tests and [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs) for activity unit tests, as well as integration helpers that cover all necessary scenarios.
|
||||
A: No. The repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) for integration tests and [`ActivityTestFixture`](../../src/common/Elsa.Testing.Shared/ActivityTestFixture.cs) for activity unit tests, as well as integration helpers that cover all necessary scenarios. For async workflows in component tests, use [`AsyncWorkflowRunner`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Services/AsyncWorkflowRunner.cs).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -287,9 +374,9 @@ A: No. The repo provides [`RunAsync`](../../src/modules/Elsa.Workflows.Core/Cont
|
|||
|
||||
Search the `test/` tree for examples that follow the above patterns:
|
||||
|
||||
- Unit test activity examples: `test/unit/Elsa.Activities.UnitTests` (look for [`ExecuteActivityAsync`](../../test/unit/Elsa.Activities.UnitTests/Helpers/ActivityTestHelper.cs) usage).
|
||||
- Integration workflow examples: `test/integration/Elsa.*.IntegrationTests` (look for [`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) and [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) usage).
|
||||
- Component scenarios exercising persistence: `test/component/Elsa.Workflows.ComponentTests (look for [`AppComponentTest`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Abstractions/AppComponentTest.cs) scaffolds and [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) assertions).
|
||||
- **Unit test activity examples:** `test/unit/Elsa.Activities.UnitTests` (look for [`ActivityTestFixture`](../../src/common/Elsa.Testing.Shared/ActivityTestFixture.cs) usage)
|
||||
- **Integration workflow examples:** `test/integration/Elsa.*.IntegrationTests` (look for [`PopulateRegistriesAsync()`](../../src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs) and [`IWorkflowRunner.RunAsync`](../../src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs) usage)
|
||||
- **Component scenarios exercising persistence:** `test/component/Elsa.Workflows.ComponentTests` (look for [`AsyncWorkflowRunner`](../../test/component/Elsa.Workflows.ComponentTests/Helpers/Services/AsyncWorkflowRunner.cs) and [`IWorkflowInstanceStore`](../../src/modules/Elsa.Workflows.Management/Contracts/IWorkflowInstanceStore.cs) assertions)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="ActivityExecutionContext"/> to enhance its functionality.
|
||||
/// </summary>
|
||||
public static class ActivityExecutionContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the specified activity is scheduled in the workflow execution context.
|
||||
/// </summary>
|
||||
/// <param name="activityExecutionContext">The activity execution context instance used for checking the scheduled activity.</param>
|
||||
/// <param name="activity">The activity to check for scheduling.</param>
|
||||
/// <returns>True if the specified activity is scheduled; otherwise, false.</returns>
|
||||
public static bool HasScheduledActivity(this ActivityExecutionContext activityExecutionContext, IActivity activity)
|
||||
{
|
||||
return activityExecutionContext.WorkflowExecutionContext.Scheduler.Find(x => x.Activity == activity) != null;
|
||||
}
|
||||
}
|
||||
174
src/common/Elsa.Testing.Shared/ActivityTestFixture.cs
Normal file
174
src/common/Elsa.Testing.Shared/ActivityTestFixture.cs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
using Elsa.Common;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Services;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Http.Parsers;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Resilience;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.CommitStates;
|
||||
using Elsa.Workflows.Management.Providers;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.PortResolvers;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Elsa.Http;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// A test fixture for unit testing activities in isolation.
|
||||
/// Provides a fluent API to configure services, variables, and execution context.
|
||||
/// </summary>
|
||||
public class ActivityTestFixture
|
||||
{
|
||||
private Action<ActivityExecutionContext>? _configureContextAction;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityTestFixture"/> class.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to test</param>
|
||||
public ActivityTestFixture(IActivity activity)
|
||||
{
|
||||
Activity = activity;
|
||||
Services = new ServiceCollection();
|
||||
AddCoreWorkflowServices(Services);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the activity being tested within the context of the activity test fixture.
|
||||
/// Provides access to the activity for configuration, execution, and validation purposes.
|
||||
/// </summary>
|
||||
public IActivity Activity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service collection for registering additional services.
|
||||
/// Use this to add services required by the activity under test.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public IServiceCollection Services { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configures the service collection using a fluent action.
|
||||
/// </summary>
|
||||
/// <param name="configure">Action to configure the service collection</param>
|
||||
/// <returns>The fixture instance for method chaining</returns>
|
||||
public ActivityTestFixture ConfigureServices(Action<IServiceCollection> configure)
|
||||
{
|
||||
configure(Services);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the activity execution context before execution.
|
||||
/// Multiple calls to this method will chain the configuration actions together.
|
||||
/// </summary>
|
||||
/// <param name="configure">Action to configure the activity execution context</param>
|
||||
/// <returns>The fixture instance for method chaining</returns>
|
||||
[UsedImplicitly]
|
||||
public ActivityTestFixture ConfigureContext(Action<ActivityExecutionContext> configure)
|
||||
{
|
||||
_configureContextAction += configure;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the activity and returns the execution context.
|
||||
/// </summary>
|
||||
/// <returns>The ActivityExecutionContext after execution</returns>
|
||||
public async Task<ActivityExecutionContext> ExecuteAsync()
|
||||
{
|
||||
var context = await BuildAsync();
|
||||
|
||||
// Set up variables and inputs, then execute the activity
|
||||
await SetupExistingVariablesAsync(Activity, context);
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
await Activity.ExecuteAsync(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the ActivityExecutionContext without executing the activity.
|
||||
/// </summary>
|
||||
public async Task<ActivityExecutionContext> BuildAsync()
|
||||
{
|
||||
var serviceProvider = Services.BuildServiceProvider();
|
||||
var activityRegistry = serviceProvider.GetRequiredService<IActivityRegistry>();
|
||||
var workflowGraphBuilder = serviceProvider.GetRequiredService<IWorkflowGraphBuilder>();
|
||||
|
||||
await activityRegistry.RegisterAsync(Activity.GetType());
|
||||
|
||||
var workflow = Workflow.FromActivity(Activity);
|
||||
var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
|
||||
|
||||
// Create workflow execution context using the static factory method
|
||||
var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(
|
||||
serviceProvider,
|
||||
workflowGraph,
|
||||
$"test-instance-{Guid.NewGuid()}",
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
// Create ActivityExecutionContext for the actual activity we want to test
|
||||
var context = await workflowExecutionContext.CreateActivityExecutionContextAsync(Activity);
|
||||
|
||||
// Apply any context configuration action
|
||||
_configureContextAction?.Invoke(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up existing variables found on the activity in the execution context.
|
||||
/// This is necessary because in unit tests, variables need to be initialized.
|
||||
/// </summary>
|
||||
private static Task SetupExistingVariablesAsync(IActivity activity, ActivityExecutionContext context)
|
||||
{
|
||||
var activityType = activity.GetType();
|
||||
var variableProperties = activityType.GetProperties()
|
||||
.Where(p => p.PropertyType.IsGenericType &&
|
||||
p.PropertyType.GetGenericTypeDefinition() == typeof(Variable<>))
|
||||
.ToList();
|
||||
|
||||
foreach (var variable in variableProperties.Select(property => (Variable)property.GetValue(activity)!))
|
||||
{
|
||||
variable.Set(context.ExpressionExecutionContext, variable.Value);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void AddCoreWorkflowServices(IServiceCollection services)
|
||||
{
|
||||
services.AddLogging();
|
||||
services.AddSingleton<ISystemClock>(_ => Substitute.For<ISystemClock>());
|
||||
services.AddSingleton<INotificationSender>(_ => Substitute.For<INotificationSender>());
|
||||
services.AddSingleton<IActivityVisitor, ActivityVisitor>();
|
||||
services.AddScoped<IExpressionEvaluator, ExpressionEvaluator>();
|
||||
services.AddSingleton<IWellKnownTypeRegistry, WellKnownTypeRegistry>();
|
||||
services.AddSingleton<IActivityDescriber, ActivityDescriber>();
|
||||
services.AddSingleton<IPropertyDefaultValueResolver, PropertyDefaultValueResolver>();
|
||||
services.AddSingleton<IActivityFactory, ActivityFactory>();
|
||||
services.AddSingleton<IPropertyUIHandlerResolver, PropertyUIHandlerResolver>();
|
||||
services.AddSingleton<IActivityRegistry, ActivityRegistry>();
|
||||
services.AddScoped<IActivityRegistryLookupService, ActivityRegistryLookupService>();
|
||||
services.AddScoped<IIdentityGraphService, IdentityGraphService>();
|
||||
services.AddScoped<IWorkflowGraphBuilder, WorkflowGraphBuilder>();
|
||||
services.AddScoped<IActivityResolver, PropertyBasedActivityResolver>();
|
||||
services.AddScoped<IActivityResolver, SwitchActivityResolver>();
|
||||
services.AddScoped<DefaultActivityInputEvaluator>();
|
||||
services.AddSingleton<IExpressionDescriptorProvider, DefaultExpressionDescriptorProvider>();
|
||||
services.AddSingleton<IExpressionDescriptorRegistry, ExpressionDescriptorRegistry>();
|
||||
services.AddSingleton<IIdentityGenerator>(_ => Substitute.For<IIdentityGenerator>());
|
||||
services.AddSingleton<IHasher>(_ => Substitute.For<IHasher>());
|
||||
services.AddSingleton<ICommitStateHandler>(_ => Substitute.For<ICommitStateHandler>());
|
||||
services.AddSingleton<IActivitySchedulerFactory, ActivitySchedulerFactory>();
|
||||
services.AddSingleton<IWorkflowExecutionContextSchedulerStrategy, FakeWorkflowExecutionContextSchedulerStrategy>();
|
||||
services.AddSingleton<IActivityExecutionContextSchedulerStrategy, FakeActivityExecutionContextSchedulerStrategy>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// General extension methods for ActivityTestFixture.
|
||||
/// </summary>
|
||||
public static class ActivityTestFixtureExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that an activity has the expected attribute configuration.
|
||||
/// This is useful for ensuring activities are properly decorated with metadata.
|
||||
/// </summary>
|
||||
/// <param name="fixture">The test fixture (unused but enables extension method syntax)</param>
|
||||
/// <param name="expectedNamespace">Expected namespace (e.g., "Elsa")</param>
|
||||
/// /// <param name="expectedKind">Expected activity kind</param>
|
||||
/// <param name="expectedCategory">Expected category (e.g., "HTTP")</param>
|
||||
/// <param name="expectedDisplayName">Expected display name</param>
|
||||
/// <param name="expectedDescription">Expected description</param>
|
||||
public static void AssertActivityAttributes(
|
||||
this ActivityTestFixture fixture,
|
||||
string expectedNamespace,
|
||||
ActivityKind expectedKind,
|
||||
string? expectedCategory = null,
|
||||
string? expectedDisplayName = null,
|
||||
string? expectedDescription = null
|
||||
)
|
||||
{
|
||||
var activityType = fixture.Activity.GetType();
|
||||
var activityAttribute = activityType.GetCustomAttributes(typeof(ActivityAttribute), false)
|
||||
.Cast<ActivityAttribute>().FirstOrDefault();
|
||||
|
||||
Assert.NotNull(activityAttribute);
|
||||
Assert.Equal(expectedNamespace, activityAttribute.Namespace);
|
||||
Assert.Equal(expectedKind, activityAttribute.Kind);
|
||||
|
||||
if (expectedCategory != null) Assert.Equal(expectedCategory, activityAttribute.Category);
|
||||
if (expectedDescription != null) Assert.Equal(expectedDescription, activityAttribute.Description);
|
||||
if (expectedDisplayName != null) Assert.Equal(expectedDisplayName, activityAttribute.DisplayName);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,9 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.abstractions" />
|
||||
<PackageReference Include="xunit.assert" />
|
||||
<PackageReference Include="xunit.extensibility.core" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Options;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a fake implementation of the <see cref="IActivityExecutionContextSchedulerStrategy"/> interface,
|
||||
/// designed for testing purposes. This class allows scheduling of activities within an activity execution context
|
||||
/// and enables control over workflows in a simulated environment.
|
||||
/// </summary>
|
||||
public class FakeActivityExecutionContextSchedulerStrategy : IActivityExecutionContextSchedulerStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Schedules the provided activity for execution within the given activity execution context.
|
||||
/// </summary>
|
||||
/// <param name="context">The current activity execution context in which to schedule the activity.</param>
|
||||
/// <param name="activity">The activity to be scheduled. Can be null, in which case no operation is performed.</param>
|
||||
/// <param name="owner">The owner activity execution context, if any.</param>
|
||||
/// <param name="options">Optional scheduling options to influence how the activity is scheduled.</param>
|
||||
public Task ScheduleActivityAsync(ActivityExecutionContext context, IActivity? activity, ActivityExecutionContext? owner, ScheduleWorkOptions? options = null)
|
||||
{
|
||||
if (activity == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var activityNode = new ActivityNode(activity!, "");
|
||||
return ScheduleActivityAsync(context, activityNode, owner, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules the provided activity for execution within the specified activity execution context using the given scheduling strategy.
|
||||
/// </summary>
|
||||
/// <param name="context">The activity execution context in which the activity will be scheduled.</param>
|
||||
/// <param name="activityNode">The activity node to be scheduled. If null, no operation is performed.</param>
|
||||
/// <param name="owner">The owner activity execution context, if any, that triggered the scheduling request.</param>
|
||||
/// <param name="options">Optional scheduling parameters to customize the scheduling behavior.</param>
|
||||
public Task ScheduleActivityAsync(ActivityExecutionContext context, ActivityNode? activityNode, ActivityExecutionContext? owner = null, ScheduleWorkOptions? options = null)
|
||||
{
|
||||
if (activityNode == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
context.WorkflowExecutionContext.Schedule(activityNode!, owner ?? context, options);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Options;
|
||||
|
||||
namespace Elsa.Testing.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Bypasses validation and schedules the activity immediately.
|
||||
/// </summary>
|
||||
public class FakeWorkflowExecutionContextSchedulerStrategy : IWorkflowExecutionContextSchedulerStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Schedules an activity within the workflow execution context.
|
||||
/// </summary>
|
||||
/// <param name="context">The workflow execution context within which the activity will be scheduled.</param>
|
||||
/// <param name="activityNode">The activity node representing the activity to be scheduled.</param>
|
||||
/// <param name="owner">The execution context owning the activity execution.</param>
|
||||
/// <param name="options">Optional scheduling options for configuring the behavior of the scheduling process.</param>
|
||||
/// <returns>Returns an <see cref="ActivityWorkItem"/> representing the scheduled activity work item.</returns>
|
||||
public ActivityWorkItem Schedule(WorkflowExecutionContext context, ActivityNode activityNode, ActivityExecutionContext owner, ScheduleWorkOptions? options = null)
|
||||
{
|
||||
var workItem = new ActivityWorkItem(activityNode.Activity, owner);
|
||||
context.Scheduler.Schedule(workItem);
|
||||
return workItem;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,37 +4,25 @@ using Elsa.Expressions.Models;
|
|||
namespace Elsa.Expressions.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ExpressionEvaluator : IExpressionEvaluator
|
||||
public class ExpressionEvaluator(IExpressionDescriptorRegistry registry, IServiceProvider serviceProvider) : IExpressionEvaluator
|
||||
{
|
||||
private readonly IExpressionDescriptorRegistry _registry;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExpressionEvaluator"/> class.
|
||||
/// </summary>
|
||||
public ExpressionEvaluator(IExpressionDescriptorRegistry registry, IServiceProvider serviceProvider)
|
||||
{
|
||||
_registry = registry;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<T?> EvaluateAsync<T>(Expression expression, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options = default)
|
||||
public async ValueTask<T?> EvaluateAsync<T>(Expression expression, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options = null)
|
||||
{
|
||||
return (T?)await EvaluateAsync(expression, typeof(T), context, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<object?> EvaluateAsync(Expression expression, Type returnType, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options = default)
|
||||
public async ValueTask<object?> EvaluateAsync(Expression expression, Type returnType, ExpressionExecutionContext context, ExpressionEvaluatorOptions? options = null)
|
||||
{
|
||||
var expressionType = expression.Type;
|
||||
var expressionDescriptor = _registry.Find(expressionType);
|
||||
var expressionDescriptor = registry.Find(expressionType);
|
||||
|
||||
if (expressionDescriptor == null)
|
||||
throw new Exception($"Could not find an descriptor for expression type \"{expressionType}\".");
|
||||
throw new($"Could not find a descriptor for expression type \"{expressionType}\".");
|
||||
|
||||
var handler = expressionDescriptor.HandlerFactory(_serviceProvider);
|
||||
options ??= new ExpressionEvaluatorOptions();
|
||||
var handler = expressionDescriptor.HandlerFactory(serviceProvider);
|
||||
options ??= new();
|
||||
return await handler.EvaluateAsync(expression, returnType, context, options);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,9 +27,16 @@ public class Parallel : Container
|
|||
/// <inheritdoc />
|
||||
protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context)
|
||||
{
|
||||
// If there are no activities, complete immediately
|
||||
if (Activities.Count == 0)
|
||||
{
|
||||
await context.CompleteActivityAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
context.SetProperty(ScheduledChildrenProperty, Activities.Count);
|
||||
|
||||
foreach (var activity in Activities)
|
||||
|
||||
foreach (var activity in Activities)
|
||||
await context.ScheduleActivityAsync(activity, OnChildCompleted);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -314,10 +314,8 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
/// <param name="options">The options used to schedule the activity.</param>
|
||||
public async ValueTask ScheduleActivityAsync(IActivity? activity, ActivityExecutionContext? owner, ScheduleWorkOptions? options = null)
|
||||
{
|
||||
var activityNode = activity != null
|
||||
? WorkflowExecutionContext.FindNodeByActivity(activity) ?? throw new InvalidOperationException("The specified activity is not part of the workflow.")
|
||||
: null;
|
||||
await ScheduleActivityAsync(activityNode, owner, options);
|
||||
var schedulerStrategy = GetRequiredService<IActivityExecutionContextSchedulerStrategy>();
|
||||
await schedulerStrategy.ScheduleActivityAsync(this, activity, owner, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -326,55 +324,10 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
/// <param name="activityNode">The activity node to schedule.</param>
|
||||
/// <param name="owner">The activity execution context that owns the scheduled activity.</param>
|
||||
/// <param name="options">The options used to schedule the activity.</param>
|
||||
public async ValueTask ScheduleActivityAsync(ActivityNode? activityNode, ActivityExecutionContext? owner = null, ScheduleWorkOptions? options = null)
|
||||
public async Task ScheduleActivityAsync(ActivityNode? activityNode, ActivityExecutionContext? owner = null, ScheduleWorkOptions? options = null)
|
||||
{
|
||||
if (this.GetIsBackgroundExecution())
|
||||
{
|
||||
// Validate that the specified activity is part of the workflow.
|
||||
if (activityNode != null && !WorkflowExecutionContext.NodeActivityLookup.ContainsKey(activityNode.Activity))
|
||||
throw new InvalidOperationException("The specified activity is not part of the workflow.");
|
||||
|
||||
var scheduledActivity = new ScheduledActivity
|
||||
{
|
||||
ActivityNodeId = activityNode?.NodeId,
|
||||
OwnerActivityInstanceId = owner?.Id,
|
||||
Options = options != null
|
||||
? new ScheduledActivityOptions
|
||||
{
|
||||
CompletionCallback = options?.CompletionCallback?.Method.Name,
|
||||
Tag = options?.Tag,
|
||||
ExistingActivityInstanceId = options?.ExistingActivityExecutionContext?.Id,
|
||||
PreventDuplicateScheduling = options?.PreventDuplicateScheduling ?? false,
|
||||
Variables = options?.Variables?.ToList(),
|
||||
Input = options?.Input
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
var scheduledActivities = this.GetBackgroundScheduledActivities().ToList();
|
||||
scheduledActivities.Add(scheduledActivity);
|
||||
this.SetBackgroundScheduledActivities(scheduledActivities);
|
||||
return;
|
||||
}
|
||||
|
||||
var completionCallback = options?.CompletionCallback;
|
||||
owner ??= this;
|
||||
|
||||
if (activityNode == null)
|
||||
{
|
||||
if (completionCallback != null)
|
||||
{
|
||||
Tag = options?.Tag;
|
||||
var completedContext = new ActivityCompletedContext(this, this);
|
||||
await completionCallback(completedContext);
|
||||
}
|
||||
else
|
||||
await owner.CompleteActivityAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
WorkflowExecutionContext.Schedule(activityNode, owner, options);
|
||||
var schedulerStrategy = GetRequiredService<IActivityExecutionContextSchedulerStrategy>();
|
||||
await schedulerStrategy.ScheduleActivityAsync(this, activityNode, owner, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Options;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a strategy for scheduling activities in a workflow execution context.
|
||||
/// </summary>
|
||||
public interface IActivityExecutionContextSchedulerStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Schedules an activity for execution based on the specified context, activity, owner, and scheduling options.
|
||||
/// </summary>
|
||||
/// <param name="context">The execution context of the activity.</param>
|
||||
/// <param name="activity">The activity to be scheduled. Can be null if no specific activity is targeted.</param>
|
||||
/// <param name="owner">The context of the owning activity, if applicable. Can be null.</param>
|
||||
/// <param name="options">Optional scheduling options for the activity.</param>
|
||||
Task ScheduleActivityAsync(
|
||||
ActivityExecutionContext context,
|
||||
IActivity? activity,
|
||||
ActivityExecutionContext? owner,
|
||||
ScheduleWorkOptions? options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Schedules an activity node for execution within the specified activity execution context.
|
||||
/// </summary>
|
||||
/// <param name="context">The execution context in which the activity node will be scheduled.</param>
|
||||
/// <param name="activityNode">The activity node to be scheduled. Can be null if no specific node is targeted.</param>
|
||||
/// <param name="owner">The context of the owning activity execution, if applicable. Can be null.</param>
|
||||
/// <param name="options">Optional settings for scheduling the activity node.</param>
|
||||
Task ScheduleActivityAsync(
|
||||
ActivityExecutionContext context,
|
||||
ActivityNode? activityNode,
|
||||
ActivityExecutionContext? owner = null,
|
||||
ScheduleWorkOptions? options = null);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Options;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a strategy interface for scheduling activities within the context of a workflow execution.
|
||||
/// </summary>
|
||||
public interface IWorkflowExecutionContextSchedulerStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Schedules an <see cref="ActivityWorkItem"/> in the context of the provided workflow execution.
|
||||
/// </summary>
|
||||
/// <param name="context">The execution context of the workflow in which the activity is being scheduled.</param>
|
||||
/// <param name="activityNode">The node representing the activity to be scheduled.</param>
|
||||
/// <param name="owner">The <see cref="ActivityExecutionContext"/> that owns the scheduled activity.</param>
|
||||
/// <param name="options">Optional parameters for scheduling the work item.</param>
|
||||
/// <returns>The scheduled <see cref="ActivityWorkItem"/>.</returns>
|
||||
ActivityWorkItem Schedule(WorkflowExecutionContext context,
|
||||
ActivityNode activityNode,
|
||||
ActivityExecutionContext owner,
|
||||
ScheduleWorkOptions? options = null);
|
||||
}
|
||||
|
|
@ -459,7 +459,15 @@ public static partial class ActivityExecutionContextExtensions
|
|||
return context.Metadata.TryGetValue(ExtensionsMetadataKey, out var value) ? value as Dictionary<string, object?> : null;
|
||||
}
|
||||
|
||||
public static object? GetExecutionOutput<TProp>(this ActivityExecutionContext context, Expression<Func<Activity, TProp>> property)
|
||||
/// <summary>
|
||||
/// Gets the output of an activity even when the context is not the same as the activity's context.
|
||||
/// This is useful when you want to get the output of a previously executed activity.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="property"></param>
|
||||
/// <typeparam name="TProp"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static object? GetActivityOutput<TProp>(this ActivityExecutionContext context, Expression<Func<TProp>> property)
|
||||
{
|
||||
if (property.Body is not MemberExpression memberExpr)
|
||||
{
|
||||
|
|
@ -467,10 +475,10 @@ public static partial class ActivityExecutionContextExtensions
|
|||
}
|
||||
|
||||
var propertyName = memberExpr.Member.Name;
|
||||
|
||||
// Treat both null and 0 (for numeric types) as "unset" values.
|
||||
|
||||
var value = context.Get(propertyName);
|
||||
if (value != null && !(value is int i && i == 0) && !(value is long l && l == 0L) && !(value is double d && d == 0.0) && !(value is float f && f == 0.0f))
|
||||
|
||||
if (value != null)
|
||||
return value;
|
||||
|
||||
var registry = context.WorkflowExecutionContext.GetActivityOutputRegister();
|
||||
|
|
@ -478,6 +486,6 @@ public static partial class ActivityExecutionContextExtensions
|
|||
return registry.FindOutputByActivityInstanceId(context.Id, propertyName);
|
||||
}
|
||||
|
||||
internal static bool GetHasEvaluatedProperties(this ActivityExecutionContext context) => context.TransientProperties.TryGetValue<bool>("HasEvaluatedProperties", out var value) && value;
|
||||
internal static void SetHasEvaluatedProperties(this ActivityExecutionContext context) => context.TransientProperties["HasEvaluatedProperties"] = true;
|
||||
public static bool GetHasEvaluatedProperties(this ActivityExecutionContext context) => context.TransientProperties.TryGetValue<bool>("HasEvaluatedProperties", out var value) && value;
|
||||
public static void SetHasEvaluatedProperties(this ActivityExecutionContext context) => context.TransientProperties["HasEvaluatedProperties"] = true;
|
||||
}
|
||||
|
|
@ -109,30 +109,8 @@ public static class WorkflowExecutionContextExtensions
|
|||
ActivityExecutionContext owner,
|
||||
ScheduleWorkOptions? options = null)
|
||||
{
|
||||
// Validate that the specified activity is part of the workflow.
|
||||
if (!workflowExecutionContext.NodeActivityLookup.ContainsKey(activityNode.Activity))
|
||||
throw new InvalidOperationException("The specified activity is not part of the workflow.");
|
||||
|
||||
var scheduler = workflowExecutionContext.Scheduler;
|
||||
|
||||
if (options?.PreventDuplicateScheduling == true)
|
||||
{
|
||||
// Check if the activity is already scheduled for the specified owner.
|
||||
var existingWorkItem = scheduler.Find(x => x.Activity.NodeId == activityNode.NodeId && x.Owner == owner);
|
||||
|
||||
if (existingWorkItem != null)
|
||||
return existingWorkItem;
|
||||
}
|
||||
|
||||
var activity = activityNode.Activity;
|
||||
var tag = options?.Tag;
|
||||
var workItem = new ActivityWorkItem(activity, owner, tag, options?.Variables, options?.ExistingActivityExecutionContext, options?.Input);
|
||||
var completionCallback = options?.CompletionCallback;
|
||||
|
||||
workflowExecutionContext.AddCompletionCallback(owner, activityNode, completionCallback, tag);
|
||||
scheduler.Schedule(workItem);
|
||||
|
||||
return workItem;
|
||||
var schedulerStrategy = workflowExecutionContext.GetRequiredService<IWorkflowExecutionContextSchedulerStrategy>();
|
||||
return schedulerStrategy.Schedule(workflowExecutionContext, activityNode, owner, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -170,6 +170,8 @@ public class WorkflowsFeature : FeatureBase
|
|||
.AddScoped<IWorkflowGraphBuilder, WorkflowGraphBuilder>()
|
||||
.AddScoped<IWorkflowStateExtractor, WorkflowStateExtractor>()
|
||||
.AddScoped<IActivitySchedulerFactory, ActivitySchedulerFactory>()
|
||||
.AddSingleton<IWorkflowExecutionContextSchedulerStrategy, WorkflowExecutionContextSchedulerStrategy>()
|
||||
.AddSingleton<IActivityExecutionContextSchedulerStrategy, ActivityExecutionContextSchedulerStrategy>()
|
||||
.AddScoped(CommitStateHandler)
|
||||
.AddSingleton<IHasher, Hasher>()
|
||||
.AddSingleton<IStimulusHasher, StimulusHasher>()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Options;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ActivityExecutionContextSchedulerStrategy : IActivityExecutionContextSchedulerStrategy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task ScheduleActivityAsync(ActivityExecutionContext context, IActivity? activity, ActivityExecutionContext? owner, ScheduleWorkOptions? options = null)
|
||||
{
|
||||
var activityNode = activity != null
|
||||
? context.WorkflowExecutionContext.FindNodeByActivity(activity) ?? throw new InvalidOperationException("The specified activity is not part of the workflow.")
|
||||
: null;
|
||||
await ScheduleActivityAsync(context, activityNode, owner, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ScheduleActivityAsync(ActivityExecutionContext context, ActivityNode? activityNode, ActivityExecutionContext? owner = null, ScheduleWorkOptions? options = null)
|
||||
{
|
||||
var workflowExecutionContext = context.WorkflowExecutionContext;
|
||||
if (context.GetIsBackgroundExecution())
|
||||
{
|
||||
// Validate that the specified activity is part of the workflow.
|
||||
if (activityNode != null && !workflowExecutionContext.NodeActivityLookup.ContainsKey(activityNode.Activity))
|
||||
throw new InvalidOperationException("The specified activity is not part of the workflow.");
|
||||
|
||||
var scheduledActivity = new ScheduledActivity
|
||||
{
|
||||
ActivityNodeId = activityNode?.NodeId,
|
||||
OwnerActivityInstanceId = owner?.Id,
|
||||
Options = options != null
|
||||
? new ScheduledActivityOptions
|
||||
{
|
||||
CompletionCallback = options?.CompletionCallback?.Method.Name,
|
||||
Tag = options?.Tag,
|
||||
ExistingActivityInstanceId = options?.ExistingActivityExecutionContext?.Id,
|
||||
PreventDuplicateScheduling = options?.PreventDuplicateScheduling ?? false,
|
||||
Variables = options?.Variables?.ToList(),
|
||||
Input = options?.Input
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
var scheduledActivities = context.GetBackgroundScheduledActivities().ToList();
|
||||
scheduledActivities.Add(scheduledActivity);
|
||||
context.SetBackgroundScheduledActivities(scheduledActivities);
|
||||
return;
|
||||
}
|
||||
|
||||
var completionCallback = options?.CompletionCallback;
|
||||
owner ??= context;
|
||||
|
||||
if (activityNode == null)
|
||||
{
|
||||
if (completionCallback != null)
|
||||
{
|
||||
context.Tag = options?.Tag;
|
||||
var completedContext = new ActivityCompletedContext(context, context);
|
||||
await completionCallback(completedContext);
|
||||
}
|
||||
else
|
||||
await owner.CompleteActivityAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
workflowExecutionContext.Schedule(activityNode, owner, options);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Options;
|
||||
|
||||
namespace Elsa.Workflows;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class WorkflowExecutionContextSchedulerStrategy : IWorkflowExecutionContextSchedulerStrategy
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ActivityWorkItem Schedule(WorkflowExecutionContext context, ActivityNode activityNode, ActivityExecutionContext owner, ScheduleWorkOptions? options = null)
|
||||
{
|
||||
// Validate that the specified activity is part of the workflow.
|
||||
if (!context.NodeActivityLookup.ContainsKey(activityNode.Activity))
|
||||
throw new InvalidOperationException("The specified activity is not part of the workflow.");
|
||||
|
||||
var scheduler = context.Scheduler;
|
||||
|
||||
if (options?.PreventDuplicateScheduling == true)
|
||||
{
|
||||
// Check if the activity is already scheduled for the specified owner.
|
||||
var existingWorkItem = scheduler.Find(x => x.Activity.NodeId == activityNode.NodeId && x.Owner == owner);
|
||||
|
||||
if (existingWorkItem != null)
|
||||
return existingWorkItem;
|
||||
}
|
||||
|
||||
var activity = activityNode.Activity;
|
||||
var tag = options?.Tag;
|
||||
var workItem = new ActivityWorkItem(activity, owner, tag, options?.Variables, options?.ExistingActivityExecutionContext, options?.Input);
|
||||
var completionCallback = options?.CompletionCallback;
|
||||
|
||||
context.AddCompletionCallback(owner, activityNode, completionCallback, tag);
|
||||
scheduler.Schedule(workItem);
|
||||
|
||||
return workItem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Models;
|
||||
using Xunit.Abstractions;
|
||||
using Parallel = Elsa.Workflows.Activities.Parallel;
|
||||
|
||||
namespace Elsa.Activities.IntegrationTests;
|
||||
|
||||
public class ParallelTests
|
||||
{
|
||||
private readonly CapturingTextWriter _capturingTextWriter = new();
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ParallelTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_serviceProvider = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel executes all child activities and completes")]
|
||||
public async Task Parallel_ExecutesAllChildren_AndCompletes()
|
||||
{
|
||||
// Arrange
|
||||
var parallel = new Parallel(
|
||||
new WriteLine("Activity 1"),
|
||||
new WriteLine("Activity 2"),
|
||||
new WriteLine("Activity 3")
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _serviceProvider.RunActivityAsync(parallel);
|
||||
|
||||
// Assert
|
||||
var journal = result.Journal;
|
||||
var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel);
|
||||
|
||||
Assert.NotNull(parallelContext);
|
||||
Assert.Equal(ActivityStatus.Completed, parallelContext.Status);
|
||||
Assert.Equal(3, _capturingTextWriter.Lines.Count);
|
||||
Assert.Contains("Activity 1", _capturingTextWriter.Lines);
|
||||
Assert.Contains("Activity 2", _capturingTextWriter.Lines);
|
||||
Assert.Contains("Activity 3", _capturingTextWriter.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel completes when empty")]
|
||||
public async Task Parallel_Completes_WhenEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var parallel = new Parallel();
|
||||
|
||||
// Act
|
||||
var result = await _serviceProvider.RunActivityAsync(parallel);
|
||||
|
||||
// Assert
|
||||
var journal = result.Journal;
|
||||
var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel);
|
||||
|
||||
Assert.NotNull(parallelContext);
|
||||
Assert.Equal(ActivityStatus.Completed, parallelContext.Status);
|
||||
Assert.Empty(_capturingTextWriter.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel executes single child activity and completes")]
|
||||
public async Task Parallel_ExecutesSingleChild_AndCompletes()
|
||||
{
|
||||
// Arrange
|
||||
var parallel = new Parallel(
|
||||
new WriteLine("Single Activity")
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _serviceProvider.RunActivityAsync(parallel);
|
||||
|
||||
// Assert
|
||||
var journal = result.Journal;
|
||||
var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel);
|
||||
|
||||
Assert.NotNull(parallelContext);
|
||||
Assert.Equal(ActivityStatus.Completed, parallelContext.Status);
|
||||
Assert.Single(_capturingTextWriter.Lines);
|
||||
Assert.Equal("Single Activity", _capturingTextWriter.Lines.Single());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel executes multiple different activity types")]
|
||||
public async Task Parallel_ExecutesMixedActivityTypes_AndCompletes()
|
||||
{
|
||||
// Arrange
|
||||
var parallel = new Parallel(
|
||||
new WriteLine("First"),
|
||||
new SetName("TestName"),
|
||||
new WriteLine("Second")
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _serviceProvider.RunActivityAsync(parallel);
|
||||
|
||||
// Assert
|
||||
var journal = result.Journal;
|
||||
var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel);
|
||||
|
||||
Assert.NotNull(parallelContext);
|
||||
Assert.Equal(ActivityStatus.Completed, parallelContext.Status);
|
||||
Assert.Equal(2, _capturingTextWriter.Lines.Count);
|
||||
Assert.Contains("First", _capturingTextWriter.Lines);
|
||||
Assert.Contains("Second", _capturingTextWriter.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel completes only after all children complete")]
|
||||
public async Task Parallel_CompletesOnlyAfterAllChildrenComplete()
|
||||
{
|
||||
// Arrange
|
||||
var parallel = new Parallel(
|
||||
new WriteLine("Child 1"),
|
||||
new WriteLine("Child 2"),
|
||||
new WriteLine("Child 3"),
|
||||
new WriteLine("Child 4")
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _serviceProvider.RunActivityAsync(parallel);
|
||||
|
||||
// Assert
|
||||
var journal = result.Journal;
|
||||
var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel);
|
||||
|
||||
Assert.NotNull(parallelContext);
|
||||
Assert.Equal(ActivityStatus.Completed, parallelContext.Status);
|
||||
Assert.Equal(4, _capturingTextWriter.Lines.Count);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel executes nested Parallel activities")]
|
||||
public async Task Parallel_ExecutesNestedParallel_AndCompletes()
|
||||
{
|
||||
// Arrange
|
||||
var innerParallel = new Parallel(
|
||||
new WriteLine("Inner 1"),
|
||||
new WriteLine("Inner 2")
|
||||
);
|
||||
|
||||
var outerParallel = new Parallel(
|
||||
new WriteLine("Outer 1"),
|
||||
innerParallel,
|
||||
new WriteLine("Outer 2")
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _serviceProvider.RunActivityAsync(outerParallel);
|
||||
|
||||
// Assert
|
||||
var journal = result.Journal;
|
||||
var outerParallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity == outerParallel);
|
||||
var innerParallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity == innerParallel);
|
||||
|
||||
Assert.NotNull(outerParallelContext);
|
||||
Assert.NotNull(innerParallelContext);
|
||||
Assert.Equal(ActivityStatus.Completed, outerParallelContext.Status);
|
||||
Assert.Equal(ActivityStatus.Completed, innerParallelContext.Status);
|
||||
Assert.Equal(4, _capturingTextWriter.Lines.Count);
|
||||
Assert.Contains("Outer 1", _capturingTextWriter.Lines);
|
||||
Assert.Contains("Outer 2", _capturingTextWriter.Lines);
|
||||
Assert.Contains("Inner 1", _capturingTextWriter.Lines);
|
||||
Assert.Contains("Inner 2", _capturingTextWriter.Lines);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel remains in Running state when a child activity faults")]
|
||||
public async Task Parallel_RemainsRunning_WhenChildFaults()
|
||||
{
|
||||
// Arrange
|
||||
var parallel = new Parallel(
|
||||
new WriteLine("Before Fault"),
|
||||
new Fault { Message = new("Test fault") },
|
||||
new WriteLine("After Fault")
|
||||
);
|
||||
|
||||
// Act
|
||||
var result = await _serviceProvider.RunActivityAsync(parallel);
|
||||
|
||||
// Assert
|
||||
var journal = result.Journal;
|
||||
var parallelContext = journal.ActivityExecutionContexts.FirstOrDefault(x => x.Activity is Parallel);
|
||||
|
||||
Assert.NotNull(parallelContext);
|
||||
Assert.Equal(ActivityStatus.Running, parallelContext.Status);
|
||||
Assert.Equal(1, parallelContext.AggregateFaultCount);
|
||||
// The non-faulted activities should still execute
|
||||
Assert.Contains("Before Fault", _capturingTextWriter.Lines);
|
||||
Assert.Contains("After Fault", _capturingTextWriter.Lines);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Evaluation;
|
||||
|
||||
/// <summary>
|
||||
/// Shared test helpers for input evaluation tests.
|
||||
/// </summary>
|
||||
public static class EvaluationTestHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an ActivityExecutionContext for any activity.
|
||||
/// </summary>
|
||||
public static async Task<ActivityExecutionContext> CreateContextAsync<TActivity>(TActivity activity)
|
||||
where TActivity : IActivity
|
||||
{
|
||||
var fixture = new ActivityTestFixture(activity);
|
||||
return await fixture.BuildAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Exceptions;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using static Elsa.Workflows.IntegrationTests.Evaluation.EvaluationTestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Evaluation;
|
||||
|
||||
public class InputEvaluationErrorTests
|
||||
{
|
||||
[Theory(DisplayName = "Wraps evaluation exceptions in InputEvaluationException")]
|
||||
[InlineData(typeof(InvalidOperationException), "Expression evaluation failed")]
|
||||
[InlineData(typeof(ArgumentException), "Detailed inner error message")]
|
||||
[InlineData(typeof(TimeoutException), "Expression evaluation timed out")]
|
||||
public async Task WrapsEvaluationExceptions(Type exceptionType, string errorMessage)
|
||||
{
|
||||
// Arrange
|
||||
var exception = (Exception)Activator.CreateInstance(exceptionType, errorMessage)!;
|
||||
var writeLine = new WriteLine("Test");
|
||||
var context = await CreateContextWithMockEvaluatorAsync(writeLine, exception);
|
||||
|
||||
// Act & Assert
|
||||
var wrappedException = await Assert.ThrowsAsync<InputEvaluationException>(
|
||||
async () => await context.EvaluateInputPropertiesAsync());
|
||||
|
||||
Assert.Equal("Text", wrappedException.InputName);
|
||||
Assert.Contains("Failed to evaluate activity input 'Text'", wrappedException.Message);
|
||||
Assert.IsType(exceptionType, wrappedException.InnerException);
|
||||
Assert.Equal(errorMessage, wrappedException.InnerException.Message);
|
||||
Assert.NotNull(wrappedException.InnerException.StackTrace);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles empty expression gracefully without throwing")]
|
||||
public async Task HandlesEmptyExpressionGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine("");
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
var exception = await Record.ExceptionAsync(
|
||||
async () => await context.EvaluateInputPropertiesAsync());
|
||||
|
||||
// Assert
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
private static async Task<ActivityExecutionContext> CreateContextWithMockEvaluatorAsync(
|
||||
WriteLine writeLine,
|
||||
Exception thrownException)
|
||||
{
|
||||
var mockHandler = Substitute.For<IExpressionHandler>();
|
||||
mockHandler.EvaluateAsync(
|
||||
Arg.Any<Expression>(),
|
||||
Arg.Any<Type>(),
|
||||
Arg.Any<ExpressionExecutionContext>(),
|
||||
Arg.Any<ExpressionEvaluatorOptions>())
|
||||
.Throws(thrownException);
|
||||
|
||||
var mockDescriptor = new ExpressionDescriptor
|
||||
{
|
||||
Type = "Literal",
|
||||
HandlerFactory = _ => mockHandler
|
||||
};
|
||||
|
||||
var mockProvider = Substitute.For<IExpressionDescriptorProvider>();
|
||||
mockProvider.GetDescriptors().Returns([mockDescriptor]);
|
||||
|
||||
var fixture = new ActivityTestFixture(writeLine)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveWhere(d => d.ServiceType == typeof(IExpressionDescriptorProvider));
|
||||
services.AddSingleton(mockProvider);
|
||||
});
|
||||
|
||||
return await fixture.BuildAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Models;
|
||||
using static Elsa.Workflows.IntegrationTests.Evaluation.EvaluationTestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Evaluation;
|
||||
|
||||
public class InputEvaluationTests
|
||||
{
|
||||
[Theory(DisplayName = "Evaluates literal input values correctly")]
|
||||
[InlineData("Literal Value")]
|
||||
[InlineData("")]
|
||||
[InlineData("Special chars: !@#$%")]
|
||||
public async Task EvaluatesLiteralInput(string inputValue)
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine(inputValue);
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.ActivityState.ContainsKey("Text"));
|
||||
Assert.Equal(inputValue, context.ActivityState["Text"]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Evaluates delegate input expressions")]
|
||||
public async Task EvaluatesDelegateInput()
|
||||
{
|
||||
// Arrange
|
||||
const string original = "Original";
|
||||
var writeLine = new WriteLine(new Input<string>(() => $"TRANSFORMED_{original}"));
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("TRANSFORMED_Original", context.ActivityState["Text"]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles null input gracefully")]
|
||||
public async Task HandlesNullInput()
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine(new Input<string>(default(string)!));
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.ActivityState["Text"]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Stores evaluated value in activity state")]
|
||||
public async Task StoresEvaluatedValueInActivityState()
|
||||
{
|
||||
// Arrange
|
||||
const string expectedValue = "Test Value";
|
||||
var writeLine = new WriteLine(expectedValue);
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(writeLine.Text);
|
||||
Assert.True(context.ActivityState.ContainsKey("Text"));
|
||||
Assert.Equal(expectedValue, context.ActivityState["Text"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using static Elsa.Workflows.IntegrationTests.Evaluation.EvaluationTestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Evaluation;
|
||||
|
||||
public class InputPropertyEvaluationTests
|
||||
{
|
||||
[Fact(DisplayName = "Evaluates all auto-evaluate inputs")]
|
||||
public async Task EvaluatesAllAutoEvaluateInputs()
|
||||
{
|
||||
// Arrange
|
||||
const string expectedText = "Hello, World!";
|
||||
var writeLine = new WriteLine(expectedText);
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains(expectedText, context.ActivityState.Values);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Evaluates specific input by name")]
|
||||
public async Task EvaluatesSpecificInputByName()
|
||||
{
|
||||
// Arrange
|
||||
const string expectedValue = "Test Value";
|
||||
var writeLine = new WriteLine(expectedValue);
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
var result = await context.EvaluateInputPropertyAsync("Text");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedValue, result);
|
||||
Assert.Contains(expectedValue, context.ActivityState.Values);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Evaluates specific input by property expression")]
|
||||
public async Task EvaluatesSpecificInputByExpression()
|
||||
{
|
||||
// Arrange
|
||||
const int expectedValue = 42;
|
||||
var variable = new Variable<int>("myVar", expectedValue, "myVar");
|
||||
var setVariable = new SetVariable<int>(variable, new Input<int>(expectedValue));
|
||||
var context = await CreateContextAsync(setVariable);
|
||||
|
||||
// Act
|
||||
var result = await context.EvaluateInputPropertyAsync<SetVariable<int>, int>(x => x.Value);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedValue, result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Throws when input name not found")]
|
||||
public async Task ThrowsWhenInputNameNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine("Test");
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<Exception>(
|
||||
async () => await context.EvaluateInputPropertyAsync("NonExistentInput"));
|
||||
|
||||
Assert.Contains("No input with name NonExistentInput could be found", exception.Message);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Sets HasEvaluatedProperties flag after evaluation")]
|
||||
public async Task SetsHasEvaluatedPropertiesFlag()
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine("Test");
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
Assert.False(context.GetHasEvaluatedProperties());
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GetHasEvaluatedProperties());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using static Elsa.Workflows.IntegrationTests.Evaluation.EvaluationTestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Evaluation;
|
||||
|
||||
public class InputStateStorageTests
|
||||
{
|
||||
[Theory(DisplayName = "Stores primitive values in activity state")]
|
||||
[InlineData(42)]
|
||||
[InlineData(0)]
|
||||
[InlineData(-100)]
|
||||
public async Task StoresPrimitiveIntValues(int expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var variable = new Variable<int>("intVar", 0, "intVar");
|
||||
var setVariable = new SetVariable<int>(variable, new Input<int>(expectedValue));
|
||||
var context = await CreateContextAsync(setVariable);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains(expectedValue, context.ActivityState.Values);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Stores boolean values in activity state")]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task StoresBooleanValues(bool expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var variable = new Variable<bool>("boolVar", false, "boolVar");
|
||||
var setVariable = new SetVariable<bool>(variable, new Input<bool>(expectedValue));
|
||||
var context = await CreateContextAsync(setVariable);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains(expectedValue, context.ActivityState.Values);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Stores string values in activity state")]
|
||||
[InlineData("Test String")]
|
||||
[InlineData("")]
|
||||
public async Task StoresStringValues(string expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
var variable = new Variable<string>("stringVar", "", "stringVar");
|
||||
var setVariable = new SetVariable<string>(variable, new Input<string>(expectedValue));
|
||||
var context = await CreateContextAsync(setVariable);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Contains(expectedValue, context.ActivityState.Values);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Stores values using input descriptor name as key")]
|
||||
public async Task StoresValuesByInputDescriptorName()
|
||||
{
|
||||
// Arrange
|
||||
const string textValue = "Named Input Test";
|
||||
var writeLine = new WriteLine(textValue);
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.ActivityState.ContainsKey("Text"));
|
||||
Assert.Equal(textValue, context.ActivityState["Text"]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Stores null values correctly")]
|
||||
public async Task StoresNullValues()
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine(new Input<string>(default(string)!));
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.ActivityState.ContainsKey("Text"));
|
||||
Assert.Null(context.ActivityState["Text"]);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Overwrites previous values on re-evaluation")]
|
||||
public async Task OverwritesPreviousValuesOnReEvaluation()
|
||||
{
|
||||
// Arrange
|
||||
const string initialValue = "Initial";
|
||||
const string updatedValue = "Updated";
|
||||
var writeLine = new WriteLine(initialValue);
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act - First evaluation
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
var firstValue = context.ActivityState["Text"];
|
||||
|
||||
// Modify and re-evaluate
|
||||
writeLine.Text = new(updatedValue);
|
||||
await context.EvaluateInputPropertyAsync("Text");
|
||||
var secondValue = context.ActivityState["Text"];
|
||||
|
||||
// Assert
|
||||
Assert.Equal(initialValue, firstValue);
|
||||
Assert.Equal(updatedValue, secondValue);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Stores multiple inputs separately")]
|
||||
public async Task StoresMultipleInputsSeparately()
|
||||
{
|
||||
// Arrange
|
||||
var variable = new Variable<int>("multiVar", 100, "multiVar");
|
||||
var setVariable = new SetVariable<int>(variable, new Input<int>(200));
|
||||
var context = await CreateContextAsync(setVariable);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.ActivityState.Count >= 1);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Maintains state across multiple evaluations")]
|
||||
public async Task MaintainsStateAcrossMultipleEvaluations()
|
||||
{
|
||||
// Arrange
|
||||
const string value1 = "First";
|
||||
var writeLine1 = new WriteLine(value1);
|
||||
var context = await CreateContextAsync(writeLine1);
|
||||
|
||||
// Act - First evaluation
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
var stateCountAfterFirst = context.ActivityState.Count;
|
||||
|
||||
// Second evaluation of same input
|
||||
await context.EvaluateInputPropertyAsync("Text");
|
||||
var stateCountAfterSecond = context.ActivityState.Count;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(stateCountAfterFirst, stateCountAfterSecond);
|
||||
Assert.Equal(value1, context.ActivityState["Text"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Elsa.Workflows.Memory;
|
||||
using Elsa.Workflows.Models;
|
||||
using static Elsa.Workflows.IntegrationTests.Evaluation.EvaluationTestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Evaluation;
|
||||
|
||||
public class WrappedInputEvaluationTests
|
||||
{
|
||||
[Fact(DisplayName = "Sets memory block reference with deterministic ID")]
|
||||
public async Task SetsMemoryBlockReferenceWithDeterministicId()
|
||||
{
|
||||
// Arrange
|
||||
const string expectedValue = "Test Value";
|
||||
var writeLine = new WriteLine(new Input<string>(expectedValue));
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
var memoryReference = writeLine.Text.MemoryBlockReference();
|
||||
Assert.NotNull(memoryReference);
|
||||
var storedValue = memoryReference.Get(context.ExpressionExecutionContext);
|
||||
Assert.Equal(expectedValue, storedValue);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Handles missing memory block reference ID")]
|
||||
public async Task HandlesMissingMemoryBlockReferenceId()
|
||||
{
|
||||
// Arrange
|
||||
const string expectedValue = "Memory Test";
|
||||
var writeLine = new WriteLine(new Input<string>(expectedValue));
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
var memoryReference = writeLine.Text.MemoryBlockReference();
|
||||
Assert.NotNull(memoryReference);
|
||||
Assert.NotNull(memoryReference.Id);
|
||||
Assert.False(string.IsNullOrEmpty(memoryReference.Id));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Stores evaluated value in ExpressionExecutionContext")]
|
||||
public async Task StoresEvaluatedValueInExpressionExecutionContext()
|
||||
{
|
||||
// Arrange
|
||||
const int expectedValue = 123;
|
||||
var variable = new Variable<int>("testVar", 0, "testVar");
|
||||
var setVariable = new SetVariable<int>(variable, new Input<int>(expectedValue));
|
||||
var context = await CreateContextAsync(setVariable);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
var memoryReference = setVariable.Value.MemoryBlockReference();
|
||||
var storedValue = memoryReference.Get(context.ExpressionExecutionContext);
|
||||
Assert.Equal(expectedValue, storedValue);
|
||||
}
|
||||
|
||||
[Theory(DisplayName = "Evaluates different expression types correctly")]
|
||||
[InlineData("Literal", "Literal Test")]
|
||||
[InlineData("Delegate", "Delegate Result")]
|
||||
[InlineData("Variable", "Variable Value")]
|
||||
public async Task EvaluatesExpressionTypes(string expressionType, string expectedValue)
|
||||
{
|
||||
// Arrange
|
||||
WriteLine writeLine;
|
||||
Variable<string>? variable = null;
|
||||
|
||||
switch (expressionType)
|
||||
{
|
||||
case "Literal":
|
||||
var literal = new Literal<string>(expectedValue);
|
||||
writeLine = new(new Input<string>(literal));
|
||||
break;
|
||||
case "Delegate":
|
||||
writeLine = new(new Input<string>(() => expectedValue));
|
||||
break;
|
||||
case "Variable":
|
||||
variable = new("myVar", expectedValue, "myVar");
|
||||
writeLine = new(new Input<string>(variable));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Invalid expression type");
|
||||
}
|
||||
|
||||
var context = await CreateContextAsync(writeLine);
|
||||
|
||||
variable?.Set(context.ExpressionExecutionContext, expectedValue);
|
||||
|
||||
// Act
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedValue, context.ActivityState["Text"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
using Elsa.Activities.UnitTests.Helpers;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
// ReSharper disable MethodHasAsyncOverload
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Console;
|
||||
|
||||
|
|
@ -17,18 +18,16 @@ public class WriteLineTests
|
|||
_mockProvider.GetTextWriter().Returns(_mockTextWriter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Write_String_Literal_To_Output()
|
||||
[Theory]
|
||||
[InlineData("Hello, World!")]
|
||||
[InlineData("")]
|
||||
public async Task Should_Write_Text_To_Output(string expectedText)
|
||||
{
|
||||
// Arrange
|
||||
const string expectedText = "Hello, World!";
|
||||
var writeLine = new WriteLine(expectedText);
|
||||
|
||||
// Act
|
||||
await ActivityTestHelper.ExecuteActivityAsync(writeLine, services =>
|
||||
{
|
||||
services.AddSingleton(_mockProvider);
|
||||
});
|
||||
await ExecuteAsync(writeLine);
|
||||
|
||||
// Assert
|
||||
_mockTextWriter.Received(1).WriteLine(expectedText);
|
||||
|
|
@ -38,35 +37,15 @@ public class WriteLineTests
|
|||
public async Task Should_Write_Null_Value_To_Output()
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine(new Input<string>((string?)null));
|
||||
var writeLine = new WriteLine(new Input<string>(default(string)!));
|
||||
|
||||
// Act
|
||||
await ActivityTestHelper.ExecuteActivityAsync(writeLine, services =>
|
||||
{
|
||||
services.AddSingleton(_mockProvider);
|
||||
});
|
||||
await ExecuteAsync(writeLine);
|
||||
|
||||
// Assert
|
||||
_mockTextWriter.Received(1).WriteLine(Arg.Is<string?>(s => s == null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Write_Empty_String_To_Output()
|
||||
{
|
||||
// Arrange
|
||||
const string expectedText = "";
|
||||
var writeLine = new WriteLine(expectedText);
|
||||
|
||||
// Act
|
||||
await ActivityTestHelper.ExecuteActivityAsync(writeLine, services =>
|
||||
{
|
||||
services.AddSingleton(_mockProvider);
|
||||
});
|
||||
|
||||
// Assert
|
||||
_mockTextWriter.Received(1).WriteLine(expectedText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Use_Default_Provider_When_None_Configured()
|
||||
{
|
||||
|
|
@ -75,9 +54,15 @@ public class WriteLineTests
|
|||
var writeLine = new WriteLine(textToWrite);
|
||||
|
||||
// Act & Assert - Should not throw exception when no provider is configured
|
||||
var exception = await Record.ExceptionAsync(async () =>
|
||||
await ActivityTestHelper.ExecuteActivityAsync(writeLine));
|
||||
|
||||
var exception = await Record.ExceptionAsync(() => ExecuteAsync(writeLine));
|
||||
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(IActivity activity)
|
||||
{
|
||||
await new ActivityTestFixture(activity)
|
||||
.ConfigureServices(services => services.AddSingleton(_mockProvider))
|
||||
.ExecuteAsync();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Http\Elsa.Http.csproj" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using Elsa.Http;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Http.Parsers;
|
||||
using Elsa.Resilience;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring HTTP-related services in ActivityTestFixture.
|
||||
/// </summary>
|
||||
public static class ActivityTestFixtureHttpExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures services commonly needed for HTTP activity testing.
|
||||
/// This is a convenience method that combines HTTP services, mock HTTP client factory, and mock resilient invoker.
|
||||
/// </summary>
|
||||
/// <param name="fixture">The test fixture to configure</param>
|
||||
/// <param name="responseHandler">Handler for HTTP requests, or null to use a default OK response</param>
|
||||
/// <returns>The fixture instance for method chaining</returns>
|
||||
public static ActivityTestFixture WithHttpServices(
|
||||
this ActivityTestFixture fixture,
|
||||
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>>? responseHandler = null)
|
||||
{
|
||||
fixture.ConfigureServices(services =>
|
||||
{
|
||||
var defaultHandler = responseHandler ?? ((_, _) => Task.FromResult(CreateHttpResponse(HttpStatusCode.OK)));
|
||||
var mockHttpClientFactory = CreateMockHttpClientFactory(defaultHandler);
|
||||
|
||||
services.AddSingleton(mockHttpClientFactory);
|
||||
services.AddSingleton(CreateMockResilientActivityInvoker());
|
||||
AddHttpServices(services);
|
||||
services.AddLogging();
|
||||
});
|
||||
|
||||
return fixture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a simple HTTP response message with specified status code and optional JSON content.
|
||||
/// </summary>
|
||||
/// <param name="statusCode">The HTTP status code for the response</param>
|
||||
/// <param name="jsonContent">Optional JSON content for the response body</param>
|
||||
/// <param name="additionalHeaders">Optional additional headers to add to the response</param>
|
||||
/// <returns>An HttpResponseMessage configured with the specified parameters</returns>
|
||||
public static HttpResponseMessage CreateHttpResponse(
|
||||
HttpStatusCode statusCode,
|
||||
string? jsonContent = null,
|
||||
Dictionary<string, string>? additionalHeaders = null)
|
||||
{
|
||||
var response = new HttpResponseMessage(statusCode);
|
||||
|
||||
if (jsonContent != null)
|
||||
{
|
||||
response.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
if (additionalHeaders != null)
|
||||
{
|
||||
foreach (var header in additionalHeaders)
|
||||
{
|
||||
response.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all HTTP-related services to the service collection.
|
||||
/// This includes content factories, parsers, and HTTP client services.
|
||||
/// </summary>
|
||||
private static void AddHttpServices(IServiceCollection services)
|
||||
{
|
||||
// Add all required HTTP services
|
||||
services.AddSingleton<IHttpContentFactory, JsonContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, TextContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, XmlContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, FormUrlEncodedHttpContentFactory>();
|
||||
|
||||
// Add HTTP content parsers
|
||||
AddHttpContentParsers(services);
|
||||
|
||||
// Add other required services
|
||||
services.AddHttpClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds HTTP content parsers to the service collection.
|
||||
/// These parsers are responsible for parsing different content types in HTTP responses.
|
||||
/// </summary>
|
||||
private static void AddHttpContentParsers(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IHttpContentParser, JsonHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, PlainTextHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, XmlHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, TextHtmlHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, FileHttpContentParser>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock IResilientActivityInvoker that directly executes the provided action.
|
||||
/// Useful for testing activities that depend on resilient execution without the complexity of retry policies.
|
||||
/// </summary>
|
||||
private static IResilientActivityInvoker CreateMockResilientActivityInvoker()
|
||||
{
|
||||
var mock = Substitute.For<IResilientActivityInvoker>();
|
||||
|
||||
// Configure the mock to simply execute the provided action directly
|
||||
mock.InvokeAsync(
|
||||
Arg.Any<IResilientActivity>(),
|
||||
Arg.Any<ActivityExecutionContext>(),
|
||||
Arg.Any<Func<Task<HttpResponseMessage>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Func<Task<HttpResponseMessage>>>(2);
|
||||
return action.Invoke();
|
||||
});
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock HTTP client factory with a test handler for controlled HTTP responses.
|
||||
/// </summary>
|
||||
private static IHttpClientFactory CreateMockHttpClientFactory(
|
||||
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responseHandler)
|
||||
{
|
||||
var mockHttpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
var testHandler = new TestHttpMessageHandler(responseHandler);
|
||||
var httpClient = new HttpClient(testHandler);
|
||||
mockHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);
|
||||
return mockHttpClientFactory;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
using Elsa.Common;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Services;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Http.Parsers;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Resilience;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.CommitStates;
|
||||
using Elsa.Workflows.Management.Providers;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.PortResolvers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Elsa.Http;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for unit testing activities in isolation.
|
||||
/// Provides methods to execute activities with minimal setup, abstracting away the complexity
|
||||
/// of creating WorkflowExecutionContext and ActivityExecutionContext.
|
||||
/// </summary>
|
||||
public static class ActivityTestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes an activity in isolation for unit testing purposes and returns the execution context.
|
||||
/// This method handles all the complexity of setting up the execution context,
|
||||
/// evaluating inputs, and executing the activity.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to execute</param>
|
||||
/// <param name="configureServices">An optional action to configure the service collection for dependency injection during activity execution.</param>
|
||||
/// <returns>The ActivityExecutionContext used for execution</returns>
|
||||
public static async Task<ActivityExecutionContext> ExecuteActivityAsync(IActivity activity, Action<IServiceCollection>? configureServices = null)
|
||||
{
|
||||
var context = await CreateMinimalActivityExecutionContext(activity, configureServices);
|
||||
|
||||
// Set up variables and inputs, then execute the activity
|
||||
await SetupExistingVariablesAsync(activity, context);
|
||||
await context.EvaluateInputPropertiesAsync();
|
||||
await activity.ExecuteAsync(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all HTTP-related services to the service collection.
|
||||
/// This includes content factories, parsers, and HTTP client services.
|
||||
/// Use this method when testing HTTP activities that require full HTTP service support.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to add HTTP services to</param>
|
||||
public static void AddHttpServices(IServiceCollection services)
|
||||
{
|
||||
// Add all required HTTP services
|
||||
services.AddSingleton<IHttpContentFactory, JsonContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, TextContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, XmlContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, FormUrlEncodedHttpContentFactory>();
|
||||
|
||||
// Add HTTP content parsers
|
||||
AddHttpContentParsers(services);
|
||||
|
||||
// Add other required services
|
||||
services.AddHttpClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mock IResilientActivityInvoker that directly executes the provided action.
|
||||
/// Useful for testing activities that depend on resilient execution without the complexity of retry policies.
|
||||
/// </summary>
|
||||
/// <returns>A mock IResilientActivityInvoker configured to execute actions directly</returns>
|
||||
public static IResilientActivityInvoker CreateMockResilientActivityInvoker()
|
||||
{
|
||||
var mock = Substitute.For<IResilientActivityInvoker>();
|
||||
|
||||
// Configure the mock to simply execute the provided action directly
|
||||
mock.InvokeAsync(
|
||||
Arg.Any<IResilientActivity>(),
|
||||
Arg.Any<ActivityExecutionContext>(),
|
||||
Arg.Any<Func<Task<HttpResponseMessage>>>(),
|
||||
Arg.Any<CancellationToken>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Func<Task<HttpResponseMessage>>>(2);
|
||||
return action.Invoke();
|
||||
});
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a minimal ActivityExecutionContext suitable for isolated unit testing of activities.
|
||||
/// This helper method creates a real WorkflowExecutionContext using the minimal workflow pattern
|
||||
/// to provide proper context for activities.
|
||||
/// </summary>
|
||||
private static async Task<ActivityExecutionContext> CreateMinimalActivityExecutionContext(IActivity activity, Action<IServiceCollection>? configureServices)
|
||||
{
|
||||
// Create a minimal service provider with the required services for expression evaluation
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Add core services
|
||||
services.AddLogging();
|
||||
services.AddSingleton<ISystemClock>(_ => Substitute.For<ISystemClock>());
|
||||
services.AddSingleton<INotificationSender>(_ => Substitute.For<INotificationSender>());
|
||||
services.AddSingleton<IActivityVisitor, ActivityVisitor>();
|
||||
services.AddScoped<IExpressionEvaluator, ExpressionEvaluator>();
|
||||
services.AddSingleton<IWellKnownTypeRegistry, WellKnownTypeRegistry>();
|
||||
services.AddSingleton<IActivityDescriber, ActivityDescriber>();
|
||||
services.AddSingleton<IPropertyDefaultValueResolver, PropertyDefaultValueResolver>();
|
||||
services.AddSingleton<IActivityFactory, ActivityFactory>();
|
||||
services.AddSingleton<IPropertyUIHandlerResolver, PropertyUIHandlerResolver>();
|
||||
services.AddSingleton<IActivityRegistry, ActivityRegistry>();
|
||||
services.AddScoped<IActivityRegistryLookupService, ActivityRegistryLookupService>();
|
||||
services.AddScoped<IIdentityGraphService, IdentityGraphService>();
|
||||
services.AddScoped<IWorkflowGraphBuilder, WorkflowGraphBuilder>();
|
||||
services.AddScoped<IActivityResolver, PropertyBasedActivityResolver>();
|
||||
services.AddScoped<IActivityResolver, SwitchActivityResolver>();
|
||||
services.AddScoped<DefaultActivityInputEvaluator>();
|
||||
|
||||
// Add the default expression descriptor provider which includes Literal expressions
|
||||
services.AddSingleton<IExpressionDescriptorProvider, DefaultExpressionDescriptorProvider>();
|
||||
services.AddSingleton<IExpressionDescriptorRegistry, ExpressionDescriptorRegistry>();
|
||||
|
||||
services.AddSingleton<IIdentityGenerator>(_ => Substitute.For<IIdentityGenerator>());
|
||||
|
||||
services.AddSingleton<IHasher>(_ => Substitute.For<IHasher>());
|
||||
services.AddSingleton<ICommitStateHandler>(_ => Substitute.For<ICommitStateHandler>());
|
||||
services.AddSingleton<IActivitySchedulerFactory>(_ => Substitute.For<IActivitySchedulerFactory>());
|
||||
|
||||
// Call the configure services action if provided.
|
||||
configureServices?.Invoke(services);
|
||||
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
var activityRegistry = serviceProvider.GetRequiredService<IActivityRegistry>();
|
||||
var workflowGraphBuilder = serviceProvider.GetRequiredService<IWorkflowGraphBuilder>();
|
||||
await activityRegistry.RegisterAsync(activity.GetType());
|
||||
var workflow = Workflow.FromActivity(activity);
|
||||
var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow);
|
||||
|
||||
// Create workflow execution context using the static factory method
|
||||
var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(
|
||||
serviceProvider,
|
||||
workflowGraph,
|
||||
$"test-instance-{Guid.NewGuid()}",
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
// Create ActivityExecutionContext for the actual activity we want to test
|
||||
return await workflowExecutionContext.CreateActivityExecutionContextAsync(activity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up existing variables found on the activity in the execution context.
|
||||
/// This is necessary because in unit tests, variables need to be initialized.
|
||||
/// </summary>
|
||||
private static Task SetupExistingVariablesAsync(IActivity activity, ActivityExecutionContext context)
|
||||
{
|
||||
var activityType = activity.GetType();
|
||||
var variableProperties = activityType.GetProperties()
|
||||
.Where(p => p.PropertyType.IsGenericType &&
|
||||
p.PropertyType.GetGenericTypeDefinition() == typeof(Variable<>))
|
||||
.ToList();
|
||||
|
||||
foreach (var variable in variableProperties.Select(property => (Variable)property.GetValue(activity)!))
|
||||
{
|
||||
variable.Set(context.ExpressionExecutionContext, variable.Value);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds HTTP content parsers to the service collection.
|
||||
/// These parsers are responsible for parsing different content types in HTTP responses.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to add parsers to</param>
|
||||
private static void AddHttpContentParsers(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IHttpContentParser, JsonHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, PlainTextHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, XmlHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, TextHtmlHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, FileHttpContentParser>();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,24 +4,14 @@ namespace Elsa.Activities.UnitTests.Helpers;
|
|||
/// Custom test HTTP message handler that allows full control over HTTP responses for testing.
|
||||
/// Use this class to simulate different HTTP scenarios without making actual network calls.
|
||||
/// </summary>
|
||||
public class TestHttpMessageHandler : HttpMessageHandler
|
||||
/// <param name="sendAsyncFunc">Function that defines how to handle HTTP requests and generate responses</param>
|
||||
public class TestHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc) : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsyncFunc;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of TestHttpMessageHandler.
|
||||
/// </summary>
|
||||
/// <param name="sendAsyncFunc">Function that defines how to handle HTTP requests and generate responses</param>
|
||||
public TestHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsyncFunc)
|
||||
{
|
||||
_sendAsyncFunc = sendAsyncFunc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles HTTP requests using the provided function.
|
||||
/// </summary>
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
return _sendAsyncFunc(request, cancellationToken);
|
||||
return sendAsyncFunc(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
288
test/unit/Elsa.Activities.UnitTests/Http/SendHttpRequestTests.cs
Normal file
288
test/unit/Elsa.Activities.UnitTests/Http/SendHttpRequestTests.cs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
using System.Net;
|
||||
using Elsa.Activities.UnitTests.Helpers;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Http;
|
||||
|
||||
public class SendHttpRequestTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("GET", "https://api.example.com/data", "{\"result\": \"success\"}", 200)]
|
||||
[InlineData("POST", "https://api.example.com/create", "{\"id\": 123}", 201)]
|
||||
[InlineData("PUT", "https://api.example.com/update", "{\"updated\": true}", 200)]
|
||||
public async Task Should_Send_Request_And_Handle_Success_Response(string method, string url, string jsonResponse, int expectedStatusCode)
|
||||
{
|
||||
// Arrange
|
||||
var expectedUrl = new Uri(url);
|
||||
var expectedMethod = new HttpMethod(method);
|
||||
var expectedHttpStatusCode = (HttpStatusCode)expectedStatusCode;
|
||||
var requestCapture = new RequestCapture();
|
||||
var responseHandler = CreateResponseHandler(expectedHttpStatusCode, jsonResponse, requestCapture);
|
||||
var sendHttpRequest = CreateSendHttpRequest(expectedUrl, method);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(requestCapture.CapturedRequest);
|
||||
Assert.Equal(expectedMethod, requestCapture.CapturedRequest.Method);
|
||||
Assert.Equal(expectedUrl, requestCapture.CapturedRequest.RequestUri);
|
||||
|
||||
var statusCodeOutput = context.GetActivityOutput(() => sendHttpRequest.StatusCode);
|
||||
Assert.Equal(expectedStatusCode, statusCodeOutput);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Bearer token123")]
|
||||
[InlineData("Basic YWRtaW46cGFzcw==")]
|
||||
[InlineData("ApiKey abc123")]
|
||||
public async Task Should_Add_Authorization_Header(string authorizationHeader)
|
||||
{
|
||||
// Arrange
|
||||
var expectedUrl = new Uri("https://api.example.com/secure");
|
||||
|
||||
var requestCapture = new RequestCapture();
|
||||
var responseHandler = CreateResponseHandler(HttpStatusCode.OK, null, requestCapture);
|
||||
var sendHttpRequest = CreateSendHttpRequest(expectedUrl, authorization: authorizationHeader);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(sendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(requestCapture.CapturedRequest);
|
||||
Assert.NotNull(requestCapture.CapturedRequest.Headers.Authorization);
|
||||
Assert.Equal(authorizationHeader, requestCapture.CapturedRequest.Headers.Authorization.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new[]{200, 404}, new[]{"mockActivity200", "mockActivity404"}, "mockUnmatchedActivity", HttpStatusCode.NotFound, "mockActivity404")]
|
||||
[InlineData(new[]{200, 404}, new[]{"mockActivity200", "mockActivity404"}, "mockUnmatchedActivity", HttpStatusCode.InternalServerError, "mockUnmatchedActivity")]
|
||||
public async Task Should_Schedule_Activity_According_To_Handlers(int[] statusCodes, string[] activityNames, string handler, HttpStatusCode expectedStatusCode, string expectedScheduledActivityName)
|
||||
{
|
||||
// Arrange
|
||||
var (sendHttpRequest, childActivities) = CreateSendHttpRequestWithStatusHandlers(
|
||||
expectedStatusCodes: [(statusCodes[0], activityNames[0]), (statusCodes[1], activityNames[1])],
|
||||
unmatchedHandler: handler
|
||||
);
|
||||
|
||||
var responseHandler = CreateResponseHandler(expectedStatusCode);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
|
||||
|
||||
// Assert that the correct activity was scheduled.
|
||||
var expectedActivity = childActivities[expectedScheduledActivityName];
|
||||
var hasScheduledActivity = context.HasScheduledActivity(expectedActivity);
|
||||
Assert.True(hasScheduledActivity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Schedule_FailedToConnect_Activity_On_HttpRequestException()
|
||||
{
|
||||
// Arrange
|
||||
var (sendHttpRequest, childActivities) = CreateSendHttpRequestWithErrorHandlers(
|
||||
failedToConnect: "mockFailedToConnect"
|
||||
);
|
||||
|
||||
var responseHandler = CreateExceptionHandler<HttpRequestException>("Connection failed");
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
var expectedScheduledActivity = childActivities["mockFailedToConnect"];
|
||||
var hasScheduledExpectedActivity = context.HasScheduledActivity(expectedScheduledActivity);
|
||||
Assert.True(hasScheduledExpectedActivity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Schedule_Timeout_Activity_On_TaskCanceledException()
|
||||
{
|
||||
// Arrange
|
||||
var (sendHttpRequest, childActivities) = CreateSendHttpRequestWithErrorHandlers(
|
||||
timeout: "mockTimeout"
|
||||
);
|
||||
|
||||
var responseHandler = CreateExceptionHandler<TaskCanceledException>("Request timed out");
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
var expectedScheduledActivity = childActivities["mockTimeout"];
|
||||
var hasScheduledExpectedActivity = context.HasScheduledActivity(expectedScheduledActivity);
|
||||
Assert.True(hasScheduledExpectedActivity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Schedule_No_Activity_When_No_Status_Code_Cases_Match_And_No_Unmatched_Handler()
|
||||
{
|
||||
// Arrange
|
||||
var (configured, _) = CreateSendHttpRequestWithStatusHandlers([(200, "handler200")], unmatchedHandler: null);
|
||||
var responseHandler = CreateResponseHandler(HttpStatusCode.InternalServerError); // 500 - no match
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(configured, responseHandler);
|
||||
|
||||
// Assert
|
||||
var allScheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
Assert.Empty(allScheduledActivities);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Set_Response_Headers_Output()
|
||||
{
|
||||
// Arrange
|
||||
var expectedHeaders = new Dictionary<string, string>
|
||||
{
|
||||
{ "Custom-Header", "CustomValue" },
|
||||
{ "X-Rate-Limit", "100" }
|
||||
};
|
||||
|
||||
var responseHandler = CreateResponseHandler(HttpStatusCode.OK, additionalHeaders: expectedHeaders);
|
||||
var sendHttpRequest = CreateSendHttpRequest(new("https://api.example.com/headers"));
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(sendHttpRequest, responseHandler);
|
||||
|
||||
// Assert
|
||||
var responseHeadersObj = context.GetActivityOutput(() => sendHttpRequest.ResponseHeaders);
|
||||
var responseHeaders = responseHeadersObj as HttpHeaders;
|
||||
Assert.NotNull(responseHeaders);
|
||||
Assert.True(responseHeaders.ContainsKey("Custom-Header"));
|
||||
Assert.True(responseHeaders.ContainsKey("X-Rate-Limit"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Have_Correct_Activity_Attributes()
|
||||
{
|
||||
var fixture = new ActivityTestFixture(new SendHttpRequest());
|
||||
fixture.AssertActivityAttributes(
|
||||
expectedNamespace: "Elsa",
|
||||
expectedCategory: "HTTP",
|
||||
expectedDisplayName: "HTTP Request",
|
||||
expectedDescription: "Send an HTTP request.",
|
||||
expectedKind: ActivityKind.Task
|
||||
);
|
||||
}
|
||||
|
||||
// Private helper methods placed after all public members
|
||||
private static Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> CreateResponseHandler(
|
||||
HttpStatusCode statusCode,
|
||||
string? content = null,
|
||||
RequestCapture? requestCapture = null,
|
||||
Dictionary<string, string>? additionalHeaders = null)
|
||||
{
|
||||
return (request, _) =>
|
||||
{
|
||||
if (requestCapture != null)
|
||||
requestCapture.CapturedRequest = request;
|
||||
return Task.FromResult(ActivityTestFixtureHttpExtensions.CreateHttpResponse(statusCode, content, additionalHeaders));
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class RequestCapture
|
||||
{
|
||||
public HttpRequestMessage? CapturedRequest { get; set; }
|
||||
}
|
||||
|
||||
private static SendHttpRequest CreateSendHttpRequest(
|
||||
Uri url,
|
||||
string method = "GET",
|
||||
object? content = null,
|
||||
string? contentType = null,
|
||||
string? authorization = null)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Url = new(url),
|
||||
Method = new(method),
|
||||
Content = content != null ? new Input<object?>(content) : null!,
|
||||
ContentType = contentType != null ? new Input<string?>(contentType) : null!,
|
||||
Authorization = authorization != null ? new Input<string?>(authorization) : null!,
|
||||
ExpectedStatusCodes = new List<HttpStatusCodeCase>()
|
||||
};
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteActivityAsync(
|
||||
SendHttpRequest sendHttpRequest,
|
||||
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responseHandler)
|
||||
{
|
||||
return new ActivityTestFixture(sendHttpRequest).WithHttpServices(responseHandler).ExecuteAsync();
|
||||
}
|
||||
|
||||
private static (SendHttpRequest sendHttpRequest, Dictionary<string, IActivity> childActivities) CreateSendHttpRequestWithStatusHandlers(
|
||||
(int statusCode, string activityName)[] expectedStatusCodes,
|
||||
string? unmatchedHandler)
|
||||
{
|
||||
var childActivities = new Dictionary<string, IActivity>();
|
||||
|
||||
// Create mock activities for expected status codes
|
||||
var expectedStatusCodeCases = expectedStatusCodes.Select(x =>
|
||||
{
|
||||
var mockActivity = Substitute.For<IActivity>();
|
||||
childActivities[x.activityName] = mockActivity;
|
||||
return new HttpStatusCodeCase(x.statusCode, mockActivity);
|
||||
}).ToList();
|
||||
|
||||
// Create mock activity for unmatched handler
|
||||
var unmatchedActivity = Substitute.For<IActivity>();
|
||||
if (unmatchedHandler is not null)
|
||||
{
|
||||
childActivities[unmatchedHandler] = unmatchedActivity;
|
||||
}
|
||||
|
||||
var sendHttpRequest = new SendHttpRequest
|
||||
{
|
||||
Url = new(new Uri("https://api.example.com/test")),
|
||||
Method = new("GET"),
|
||||
ExpectedStatusCodes = expectedStatusCodeCases,
|
||||
UnmatchedStatusCode = unmatchedHandler is not null ? unmatchedActivity : null
|
||||
};
|
||||
|
||||
return (sendHttpRequest, childActivities);
|
||||
}
|
||||
|
||||
private static (SendHttpRequest sendHttpRequest, Dictionary<string, IActivity> childActivities) CreateSendHttpRequestWithErrorHandlers(
|
||||
string? failedToConnect = null,
|
||||
string? timeout = null)
|
||||
{
|
||||
var childActivities = new Dictionary<string, IActivity>();
|
||||
|
||||
IActivity? failedToConnectActivity = null;
|
||||
IActivity? timeoutActivity = null;
|
||||
|
||||
if (failedToConnect != null)
|
||||
{
|
||||
failedToConnectActivity = Substitute.For<IActivity>();
|
||||
childActivities[failedToConnect] = failedToConnectActivity;
|
||||
}
|
||||
|
||||
if (timeout != null)
|
||||
{
|
||||
timeoutActivity = Substitute.For<IActivity>();
|
||||
childActivities[timeout] = timeoutActivity;
|
||||
}
|
||||
|
||||
var sendHttpRequest = new SendHttpRequest
|
||||
{
|
||||
Url = new(new Uri("https://api.example.com/error")),
|
||||
Method = new("GET"),
|
||||
ExpectedStatusCodes = new List<HttpStatusCodeCase>(),
|
||||
FailedToConnect = failedToConnectActivity,
|
||||
Timeout = timeoutActivity
|
||||
};
|
||||
|
||||
return (sendHttpRequest, childActivities);
|
||||
}
|
||||
|
||||
private static Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> CreateExceptionHandler<TException>(string message)
|
||||
where TException : Exception
|
||||
{
|
||||
return (_, _) => throw ((TException)Activator.CreateInstance(typeof(TException), message)!);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Parallel;
|
||||
|
||||
public class ParallelTests
|
||||
{
|
||||
[Theory(DisplayName = "Parallel should schedule multiple activities")]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[InlineData(4)]
|
||||
public async Task Should_Schedule_Multiple_Activities(int activityCount)
|
||||
{
|
||||
// Arrange
|
||||
var activities = Enumerable.Range(1, activityCount)
|
||||
.Select(i => new WriteLine($"Activity {i}"))
|
||||
.Cast<IActivity>()
|
||||
.ToArray();
|
||||
|
||||
var parallel = new Workflows.Activities.Parallel(activities);
|
||||
|
||||
// Act
|
||||
var context = await new ActivityTestFixture(parallel).ExecuteAsync();
|
||||
|
||||
// Assert - All activities should be scheduled
|
||||
foreach (var activity in activities)
|
||||
{
|
||||
Assert.True(context.HasScheduledActivity(activity));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel should not schedule activities when empty")]
|
||||
public async Task Should_Not_Schedule_Activities_When_Empty()
|
||||
{
|
||||
// Arrange
|
||||
var parallel = new Workflows.Activities.Parallel();
|
||||
|
||||
// Act
|
||||
var context = await new ActivityTestFixture(parallel).ExecuteAsync();
|
||||
|
||||
// Assert - No activities should be scheduled (completion is tested in integration tests)
|
||||
var scheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
Assert.Empty(scheduledActivities);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel should schedule a single activity")]
|
||||
public async Task Should_Execute_Single_Activity()
|
||||
{
|
||||
// Arrange
|
||||
var activity = Substitute.For<IActivity>();
|
||||
var parallel = new Workflows.Activities.Parallel(activity);
|
||||
|
||||
// Act
|
||||
var context = await new ActivityTestFixture(parallel).ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(activity));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel should schedule mixed activity types")]
|
||||
public async Task Should_Schedule_Mixed_Activity_Types()
|
||||
{
|
||||
// Arrange
|
||||
var writeLine = new WriteLine("Test output");
|
||||
var testVar = new Variable<int>("testVar", 0, "testVar");
|
||||
var setVariable = new SetVariable<int>(testVar, new Input<int>(42));
|
||||
var mockActivity = Substitute.For<IActivity>();
|
||||
|
||||
var parallel = new Workflows.Activities.Parallel(writeLine, setVariable, mockActivity);
|
||||
|
||||
// Act
|
||||
var context = await new ActivityTestFixture(parallel).ExecuteAsync();
|
||||
|
||||
// Assert - All activities should be scheduled
|
||||
Assert.True(context.HasScheduledActivity(writeLine));
|
||||
Assert.True(context.HasScheduledActivity(setVariable));
|
||||
Assert.True(context.HasScheduledActivity(mockActivity));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Parallel should track scheduled children count internally")]
|
||||
public async Task Should_Set_Scheduled_Children_Property()
|
||||
{
|
||||
// Arrange
|
||||
var activity1 = new WriteLine("Activity 1");
|
||||
var activity2 = new WriteLine("Activity 2");
|
||||
var activity3 = new WriteLine("Activity 3");
|
||||
var parallel = new Workflows.Activities.Parallel(activity1, activity2, activity3);
|
||||
|
||||
// Act
|
||||
var context = await new ActivityTestFixture(parallel).ExecuteAsync();
|
||||
|
||||
// Assert - The parallel activity should track that 3 children were scheduled
|
||||
var scheduledChildrenCount = context.GetProperty<int>("ScheduledChildren");
|
||||
Assert.Equal(3, scheduledChildrenCount);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Activities.UnitTests.Helpers;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Primitives;
|
||||
|
||||
|
|
@ -13,7 +14,7 @@ public class SetVariableTests
|
|||
var setVariable = new SetVariable<int>(variable, new Input<int>(expected));
|
||||
|
||||
// Act
|
||||
var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable);
|
||||
var context = await ExecuteAsync(setVariable);
|
||||
|
||||
// Assert
|
||||
var result = variable.Get(context);
|
||||
|
|
@ -27,7 +28,7 @@ public class SetVariableTests
|
|||
var setVariable = new SetVariable<string>(null!, new Input<string>("test value"));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Record.ExceptionAsync(async () => await ActivityTestHelper.ExecuteActivityAsync(setVariable));
|
||||
var exception = await Record.ExceptionAsync(() => ExecuteAsync(setVariable));
|
||||
|
||||
Assert.NotNull(exception);
|
||||
}
|
||||
|
|
@ -40,10 +41,15 @@ public class SetVariableTests
|
|||
var setVariable = new SetVariable<string?>(variable, new Input<string?>((string?)null));
|
||||
|
||||
// Act
|
||||
var context = await ActivityTestHelper.ExecuteActivityAsync(setVariable);
|
||||
var context = await ExecuteAsync(setVariable);
|
||||
|
||||
// Assert
|
||||
var result = variable.Get(context);
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> ExecuteAsync(IActivity activity)
|
||||
{
|
||||
return new ActivityTestFixture(activity).ExecuteAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Expressions\Elsa.Expressions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Activities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
|
||||
namespace Elsa.Expressions.UnitTests.Services;
|
||||
|
||||
public class ExpressionEvaluatorTests
|
||||
{
|
||||
[Fact(DisplayName = "Evaluates expression with generic type parameter")]
|
||||
public async Task EvaluatesExpressionWithGenericTypeParameter()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var expression = new Expression("Literal", "Test Value");
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync<string>(expression, context.ExpressionExecutionContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Test Value", result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Evaluates expression with type parameter")]
|
||||
public async Task EvaluatesExpressionWithTypeParameter()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var expression = new Expression("Literal", 42);
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync(expression, typeof(int), context.ExpressionExecutionContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(42, result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Throws when expression type not found in registry")]
|
||||
public async Task ThrowsWhenExpressionTypeNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var expression = new Expression("NonExistentType", "value");
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<Exception>(async () =>
|
||||
await evaluator.EvaluateAsync<string>(expression, context.ExpressionExecutionContext));
|
||||
|
||||
Assert.Contains("Could not find a descriptor for expression type", exception.Message);
|
||||
Assert.Contains("NonExistentType", exception.Message);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Resolves expression handler via descriptor factory")]
|
||||
public async Task ResolvesExpressionHandlerViaDescriptorFactory()
|
||||
{
|
||||
// Arrange
|
||||
var mockHandler = CreateMockHandler("Mocked Result");
|
||||
var context = await CreateContextWithMockHandlerAsync("CustomType", mockHandler);
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var expression = new Expression("CustomType", "test");
|
||||
|
||||
// Act
|
||||
var result = await evaluator.EvaluateAsync<string>(expression, context.ExpressionExecutionContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Mocked Result", result);
|
||||
await mockHandler.Received(1).EvaluateAsync(
|
||||
Arg.Is<Expression>(e => e.Type == "CustomType" && e.Value as string == "test"),
|
||||
Arg.Any<Type>(),
|
||||
Arg.Any<ExpressionExecutionContext>(),
|
||||
Arg.Any<ExpressionEvaluatorOptions>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Passes non-null options as default when not provided")]
|
||||
public async Task PassesNonNullOptionsAsDefault()
|
||||
{
|
||||
// Arrange
|
||||
var mockHandler = CreateMockHandler("Result");
|
||||
var context = await CreateContextWithMockHandlerAsync("TestType", mockHandler);
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var expression = new Expression("TestType", "value");
|
||||
|
||||
// Act
|
||||
await evaluator.EvaluateAsync<string>(expression, context.ExpressionExecutionContext);
|
||||
|
||||
// Assert
|
||||
await mockHandler.Received(1).EvaluateAsync(
|
||||
Arg.Any<Expression>(),
|
||||
Arg.Any<Type>(),
|
||||
Arg.Any<ExpressionExecutionContext>(),
|
||||
Arg.Is<ExpressionEvaluatorOptions>(o => o != null));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Wraps handler exceptions in evaluation context")]
|
||||
public async Task WrapsHandlerExceptions()
|
||||
{
|
||||
// Arrange
|
||||
var mockHandler = CreateMockHandlerThatThrows(new InvalidOperationException("Handler failed"));
|
||||
var context = await CreateContextWithMockHandlerAsync("FailingType", mockHandler);
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var expression = new Expression("FailingType", "bad value");
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await evaluator.EvaluateAsync<string>(expression, context.ExpressionExecutionContext));
|
||||
|
||||
Assert.Equal("Handler failed", exception.Message);
|
||||
}
|
||||
|
||||
private static Task<ActivityExecutionContext> CreateContextAsync()
|
||||
{
|
||||
var activity = new WriteLine("Hello, World!");
|
||||
return new ActivityTestFixture(activity).BuildAsync();
|
||||
}
|
||||
|
||||
private static async Task<ActivityExecutionContext> CreateContextWithMockHandlerAsync(string expressionType, IExpressionHandler mockHandler)
|
||||
{
|
||||
var mockDescriptor = new ExpressionDescriptor
|
||||
{
|
||||
Type = expressionType,
|
||||
HandlerFactory = _ => mockHandler
|
||||
};
|
||||
|
||||
var mockProvider = Substitute.For<IExpressionDescriptorProvider>();
|
||||
mockProvider.GetDescriptors().Returns([mockDescriptor]);
|
||||
|
||||
var activity = new WriteLine("Hello, World!");
|
||||
var fixture = new ActivityTestFixture(activity)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveWhere(d => d.ServiceType == typeof(IExpressionDescriptorProvider));
|
||||
services.AddSingleton(mockProvider);
|
||||
});
|
||||
|
||||
return await fixture.BuildAsync();
|
||||
}
|
||||
|
||||
private static IExpressionHandler CreateMockHandler(object returnValue)
|
||||
{
|
||||
var mockHandler = Substitute.For<IExpressionHandler>();
|
||||
mockHandler.EvaluateAsync(
|
||||
Arg.Any<Expression>(),
|
||||
Arg.Any<Type>(),
|
||||
Arg.Any<ExpressionExecutionContext>(),
|
||||
Arg.Any<ExpressionEvaluatorOptions>())
|
||||
.Returns(returnValue);
|
||||
return mockHandler;
|
||||
}
|
||||
|
||||
private static IExpressionHandler CreateMockHandlerThatThrows(Exception exception)
|
||||
{
|
||||
var mockHandler = Substitute.For<IExpressionHandler>();
|
||||
mockHandler.EvaluateAsync(
|
||||
Arg.Any<Expression>(),
|
||||
Arg.Any<Type>(),
|
||||
Arg.Any<ExpressionExecutionContext>(),
|
||||
Arg.Any<ExpressionEvaluatorOptions>())
|
||||
.Throws(exception);
|
||||
return mockHandler;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
using Elsa.Extensions;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class FaultHandlingTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Fault_SetsExceptionAndStatus()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
var exception = new InvalidOperationException("Test error");
|
||||
|
||||
// Act
|
||||
context.Fault(exception);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ActivityStatus.Faulted, context.Status);
|
||||
Assert.Equal(exception, context.Exception);
|
||||
Assert.Equal(1, context.AggregateFaultCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecoverFromFault_ResetsFaultCount()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
var exception = new InvalidOperationException("Test error");
|
||||
context.Fault(exception);
|
||||
|
||||
// Act
|
||||
context.RecoverFromFault();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, context.AggregateFaultCount);
|
||||
Assert.Equal(ActivityStatus.Running, context.Status);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Activities;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class HierarchyNavigationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("GetAncestors")]
|
||||
[InlineData("GetDescendants")]
|
||||
public async Task HierarchyNavigation_ReturnsEmpty_WhenNoRelatives(string methodName)
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act
|
||||
var result = methodName switch
|
||||
{
|
||||
"GetAncestors" => context.GetAncestors().ToList(),
|
||||
"GetDescendants" => context.GetDescendants().ToList(),
|
||||
_ => throw new ArgumentException($"Unknown method: {methodName}")
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindParent_ReturnsNull_WhenNoMatch()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act
|
||||
var parent = context.FindParent(x => x.Activity is Sequence);
|
||||
|
||||
// Assert
|
||||
Assert.Null(parent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
using Elsa.Extensions;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class MetadataTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SetExtensionsMetadata_StoresValue()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act
|
||||
context.SetExtensionsMetadata("testKey", "testValue");
|
||||
|
||||
// Assert
|
||||
var metadata = context.GetExtensionsMetadata();
|
||||
Assert.NotNull(metadata);
|
||||
Assert.Equal("testValue", metadata["testKey"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExtensionsMetadata_ReturnsNull_WhenNotSet()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act
|
||||
var metadata = context.GetExtensionsMetadata();
|
||||
|
||||
// Assert
|
||||
Assert.Null(metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetExtensionsMetadata_UpdatesExistingValue()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
context.SetExtensionsMetadata("testKey", "value1");
|
||||
|
||||
// Act
|
||||
context.SetExtensionsMetadata("testKey", "value2");
|
||||
|
||||
// Assert
|
||||
var metadata = context.GetExtensionsMetadata();
|
||||
Assert.Equal("value2", metadata!["testKey"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Attributes;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class OutcomeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(typeof(TestActivityWithPort), "Done")]
|
||||
[InlineData(typeof(TestActivityWithNamedPort), "CustomOutcome")]
|
||||
public async Task GetOutcomeName_ReturnsExpectedName(Type activityType, string expectedOutcomeName)
|
||||
{
|
||||
// Arrange
|
||||
var activity = (Activity)Activator.CreateInstance(activityType)!;
|
||||
var context = await CreateContextAsync(activity);
|
||||
|
||||
// Act
|
||||
var outcomeName = context.GetOutcomeName("Done");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedOutcomeName, outcomeName);
|
||||
}
|
||||
|
||||
private class TestActivityWithPort : Activity
|
||||
{
|
||||
public IActivity? Done { get; set; }
|
||||
}
|
||||
|
||||
private class TestActivityWithNamedPort : Activity
|
||||
{
|
||||
[Port("CustomOutcome")]
|
||||
public IActivity? Done { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using Elsa.Extensions;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class PropertyEvaluationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)] // Initial state
|
||||
[InlineData(true)] // After setting flag
|
||||
public async Task HasEvaluatedProperties_TracksState(bool shouldSetFlag)
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
if (shouldSetFlag)
|
||||
context.SetHasEvaluatedProperties();
|
||||
|
||||
// Act
|
||||
var result = context.GetHasEvaluatedProperties();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(shouldSetFlag, result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Models;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class ResultTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SetResult_SetsResultProperty_WhenActivityImplementsIActivityWithResult()
|
||||
{
|
||||
// Arrange
|
||||
var activity = new ActivityWithResult();
|
||||
var context = await CreateContextAsync(activity);
|
||||
var testValue = "ResultValue";
|
||||
|
||||
// Act
|
||||
context.SetResult(testValue);
|
||||
|
||||
// Assert
|
||||
var result = context.Get(activity.Result);
|
||||
Assert.Equal(testValue, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetResult_ThrowsException_WhenActivityDoesNotImplementIActivityWithResult()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<Exception>(() => context.SetResult("value"));
|
||||
}
|
||||
|
||||
private class ActivityWithResult : Activity, IActivityWithResult
|
||||
{
|
||||
public Output? Result { get; set; } = new Output<object?>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Activities;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for ActivityExecutionContextExtensions unit tests.
|
||||
/// </summary>
|
||||
public static class TestHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an ActivityExecutionContext for testing purposes.
|
||||
/// </summary>
|
||||
/// <param name="activity">Optional activity to use. If null, a default WriteLine activity is created.</param>
|
||||
/// <returns>A configured ActivityExecutionContext ready for testing.</returns>
|
||||
public static Task<ActivityExecutionContext> CreateContextAsync(IActivity? activity = null)
|
||||
{
|
||||
activity ??= new WriteLine("test");
|
||||
var fixture = new ActivityTestFixture(activity);
|
||||
return fixture.BuildAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Elsa.Extensions;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class TriggerTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true, null)] // null means use context.Activity.Id
|
||||
[InlineData(false, "different-id")]
|
||||
public async Task IsTriggerOfWorkflow_ReturnsExpectedResult(bool expectedResult, string? triggerActivityId)
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
context.WorkflowExecutionContext.TriggerActivityId = triggerActivityId ?? context.Activity.Id;
|
||||
|
||||
// Act
|
||||
var result = context.IsTriggerOfWorkflow();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
using Elsa.Extensions;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class VariableTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateVariable_CreatesNewVariable_WithSpecifiedValue()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act
|
||||
var variable = context.CreateVariable("testVar", 42);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(variable);
|
||||
Assert.Equal("testVar", variable.Name);
|
||||
var value = context.GetVariable<int>("testVar");
|
||||
Assert.Equal(42, value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetVariable_CreatesOrUpdatesVariable()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act
|
||||
context.SetVariable("testVar", 10);
|
||||
context.SetVariable("testVar", 20);
|
||||
|
||||
// Assert
|
||||
var value = context.GetVariable<int>("testVar");
|
||||
Assert.Equal(20, value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetVariable_ReturnsNull_WhenVariableDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act
|
||||
var value = context.GetVariable<string>("nonExistent");
|
||||
|
||||
// Assert
|
||||
Assert.Null(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetVariableValues_ReturnsAllVariables_AcrossScopes()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
context.SetVariable("var1", "value1");
|
||||
context.SetVariable("var2", 42);
|
||||
|
||||
// Act
|
||||
var values = context.GetVariableValues();
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(values);
|
||||
Assert.True(values.ContainsKey("var1Variable") || values.ContainsKey("var1"));
|
||||
Assert.True(values.ContainsKey("var2Variable") || values.ContainsKey("var2"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
using Elsa.Extensions;
|
||||
using static Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions.TestHelpers;
|
||||
|
||||
namespace Elsa.Workflows.Core.UnitTests.Extensions.ActivityExecutionContextExtensions;
|
||||
|
||||
public class WorkflowInputTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("TestKey", "TestValue", true, "TestValue", null)] // Key exists, string value
|
||||
[InlineData("NonExistentKey", null, false, null, null)] // Key doesn't exist
|
||||
[InlineData("NumberKey", null, true, "42", 42)] // Type conversion from int to string
|
||||
public async Task TryGetWorkflowInput_HandlesVariousScenarios(
|
||||
string key,
|
||||
string? stringValue,
|
||||
bool expectedResult,
|
||||
string? expectedValue,
|
||||
object? inputValue)
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
if (stringValue != null)
|
||||
context.WorkflowExecutionContext.Input[key] = stringValue;
|
||||
else if (inputValue != null)
|
||||
context.WorkflowExecutionContext.Input[key] = inputValue;
|
||||
|
||||
// Act
|
||||
var result = context.TryGetWorkflowInput<string>(key, out var value);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedResult, result);
|
||||
Assert.Equal(expectedValue, value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetWorkflowInput_ReturnsValue_WhenKeyExists()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
context.WorkflowExecutionContext.Input["TestKey"] = "TestValue";
|
||||
|
||||
// Act
|
||||
var value = context.GetWorkflowInput<string>("TestKey");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("TestValue", value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetWorkflowInput_UsesTypeName_WhenKeyNotProvided()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
context.WorkflowExecutionContext.Input["String"] = "TestValue";
|
||||
|
||||
// Act
|
||||
var value = context.GetWorkflowInput<string>();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("TestValue", value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetWorkflowInput_ThrowsException_WhenKeyDoesNotExist()
|
||||
{
|
||||
// Arrange
|
||||
var context = await CreateContextAsync();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<KeyNotFoundException>(() => context.GetWorkflowInput<string>("NonExistentKey"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\common\Elsa.Testing.Shared\Elsa.Testing.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Workflows.Management.UnitTests.Services;
|
||||
|
||||
public class ExpressionDescriptorRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Should_Add_Descriptors_Via_Providers()
|
||||
{
|
||||
// Arrange
|
||||
var descriptor1 = CreateDescriptor("Type1");
|
||||
var descriptor2 = CreateDescriptor("Type2");
|
||||
var provider1 = CreateProvider(descriptor1);
|
||||
var provider2 = CreateProvider(descriptor2);
|
||||
|
||||
// Act
|
||||
var registry = CreateRegistry(provider1, provider2);
|
||||
|
||||
// Assert
|
||||
var allDescriptors = registry.ListAll().ToList();
|
||||
Assert.Contains(allDescriptors, d => d.Type == "Type1");
|
||||
Assert.Contains(allDescriptors, d => d.Type == "Type2");
|
||||
Assert.Equal(2, allDescriptors.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Find_Descriptor_By_Type()
|
||||
{
|
||||
// Arrange
|
||||
var descriptor = CreateDescriptor("TestType");
|
||||
var registry = CreateRegistry(CreateProvider(descriptor));
|
||||
|
||||
// Act
|
||||
var found = registry.Find("TestType");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(found);
|
||||
Assert.Equal("TestType", found.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Find_Descriptor_By_Predicate()
|
||||
{
|
||||
// Arrange
|
||||
var descriptor1 = CreateDescriptor("Type1");
|
||||
var descriptor2 = CreateDescriptor("Type2");
|
||||
var registry = CreateRegistry(CreateProvider(descriptor1, descriptor2));
|
||||
|
||||
// Act
|
||||
var found = registry.Find(d => d.Type == "Type2");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(found);
|
||||
Assert.Equal("Type2", found.Type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("NonExistentType")]
|
||||
[InlineData("")]
|
||||
[InlineData("WrongType")]
|
||||
public void Should_Return_Null_When_Descriptor_Not_Found_By_Type(string searchType)
|
||||
{
|
||||
// Arrange
|
||||
var descriptor = CreateDescriptor("ExistingType");
|
||||
var registry = CreateRegistry(CreateProvider(descriptor));
|
||||
|
||||
// Act
|
||||
var found = registry.Find(searchType);
|
||||
|
||||
// Assert
|
||||
Assert.Null(found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Return_Null_When_Descriptor_Not_Found_By_Predicate()
|
||||
{
|
||||
// Arrange
|
||||
var descriptor = CreateDescriptor("Type1");
|
||||
var registry = CreateRegistry(CreateProvider(descriptor));
|
||||
|
||||
// Act
|
||||
var found = registry.Find(d => d.Type == "NonExistent");
|
||||
|
||||
// Assert
|
||||
Assert.Null(found);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_List_All_Registered_Descriptors()
|
||||
{
|
||||
// Arrange
|
||||
var descriptor1 = CreateDescriptor("Type1");
|
||||
var descriptor2 = CreateDescriptor("Type2");
|
||||
var descriptor3 = CreateDescriptor("Type3");
|
||||
var registry = CreateRegistry(CreateProvider(descriptor1, descriptor2, descriptor3));
|
||||
|
||||
// Act
|
||||
var all = registry.ListAll().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, all.Count);
|
||||
Assert.Contains(all, d => d.Type == "Type1");
|
||||
Assert.Contains(all, d => d.Type == "Type2");
|
||||
Assert.Contains(all, d => d.Type == "Type3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Handle_Duplicate_Registrations_Last_Wins()
|
||||
{
|
||||
// Arrange - Two providers register the same type
|
||||
var handler1 = Substitute.For<IExpressionHandler>();
|
||||
var handler2 = Substitute.For<IExpressionHandler>();
|
||||
var descriptor1 = CreateDescriptor("DuplicateType", handler1);
|
||||
var descriptor2 = CreateDescriptor("DuplicateType", handler2);
|
||||
var provider1 = CreateProvider(descriptor1);
|
||||
var provider2 = CreateProvider(descriptor2);
|
||||
|
||||
// Act - provider2 is registered after provider1
|
||||
var registry = CreateRegistry(provider1, provider2);
|
||||
|
||||
// Assert - Last registration should win
|
||||
var found = registry.Find("DuplicateType");
|
||||
Assert.NotNull(found);
|
||||
Assert.Same(handler2, found.HandlerFactory(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Add_Single_Descriptor_Via_Add_Method()
|
||||
{
|
||||
// Arrange
|
||||
var registry = CreateRegistry(CreateProvider());
|
||||
var newDescriptor = CreateDescriptor("NewType");
|
||||
|
||||
// Act
|
||||
registry.Add(newDescriptor);
|
||||
|
||||
// Assert
|
||||
var found = registry.Find("NewType");
|
||||
Assert.NotNull(found);
|
||||
Assert.Equal("NewType", found.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Add_Multiple_Descriptors_Via_AddRange_Method()
|
||||
{
|
||||
// Arrange
|
||||
var registry = CreateRegistry(CreateProvider());
|
||||
var descriptors = new[]
|
||||
{
|
||||
CreateDescriptor("Range1"),
|
||||
CreateDescriptor("Range2"),
|
||||
CreateDescriptor("Range3")
|
||||
};
|
||||
|
||||
// Act
|
||||
registry.AddRange(descriptors);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(registry.Find("Range1"));
|
||||
Assert.NotNull(registry.Find("Range2"));
|
||||
Assert.NotNull(registry.Find("Range3"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)] // Empty provider list
|
||||
[InlineData(false)] // Provider with empty descriptors
|
||||
public void Should_Handle_Empty_Scenarios(bool emptyProviderList)
|
||||
{
|
||||
// Arrange & Act
|
||||
var registry = emptyProviderList
|
||||
? CreateRegistry()
|
||||
: CreateRegistry(CreateProvider());
|
||||
|
||||
// Assert
|
||||
var all = registry.ListAll().ToList();
|
||||
Assert.Empty(all);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Should_Enumerate_Descriptors_Multiple_Times()
|
||||
{
|
||||
// Arrange
|
||||
var descriptor = CreateDescriptor("TestType");
|
||||
var registry = CreateRegistry(CreateProvider(descriptor));
|
||||
|
||||
// Act
|
||||
var first = registry.ListAll().ToList();
|
||||
var second = registry.ListAll().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(first.Count, second.Count);
|
||||
Assert.Single(first);
|
||||
Assert.Single(second);
|
||||
}
|
||||
|
||||
private static ExpressionDescriptor CreateDescriptor(string type)
|
||||
{
|
||||
var handler = Substitute.For<IExpressionHandler>();
|
||||
return CreateDescriptor(type, handler);
|
||||
}
|
||||
|
||||
private static ExpressionDescriptor CreateDescriptor(string type, IExpressionHandler handler)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Type = type,
|
||||
HandlerFactory = _ => handler
|
||||
};
|
||||
}
|
||||
|
||||
private static IExpressionDescriptorProvider CreateProvider(params ExpressionDescriptor[] descriptors)
|
||||
{
|
||||
var provider = Substitute.For<IExpressionDescriptorProvider>();
|
||||
provider.GetDescriptors().Returns(descriptors);
|
||||
return provider;
|
||||
}
|
||||
|
||||
private static ExpressionDescriptorRegistry CreateRegistry(params IExpressionDescriptorProvider[] providers)
|
||||
{
|
||||
return new(providers);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue