* Add ADR for Null Tenant ID, implement tenant-agnostic logic Introduce ADR-0009 to document the use of `null` for tenant-agnostic entities, enhancing multitenancy handling. Update multitenancy features across the codebase, including EF Core query filters and ActivityRegistry, to handle null as a tenant ID, ensuring tenant-agnostic entities are accessible across all tenants. * Add multitenancy support in `ActivityTestFixture` by registering `ITenantAccessor`. * Update src/modules/Elsa.Common/Multitenancy/Implementations/DefaultTenantAccessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove unused `using Elsa.Common.Multitenancy;` from WorkflowDefinitionActivityDescriptorFactory (#7230) * Initial plan * Remove unused using Elsa.Common.Multitenancy statement Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Optimize ActivityRegistry.Find to prefer tenant-specific descriptors without performance regression (#7227) * Initial plan * Optimize Find(string type) to prefer tenant-specific descriptors with single-pass iteration Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Apply review feedback: combine if statements and add comprehensive unit tests Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Refactor tests for DRYness using theories and helper methods Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Clean up extra whitespace in test file Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Fix default tenant data visibility leak by removing NullIfEmpty conversion (#7229) * Initial plan * Remove NullIfEmpty conversion to align with ADR-0008 and ADR-0009 - Updated ElsaDbContextBase to use empty string for default tenant - Updated ApplyTenantId to stop converting empty string to null - Updated TenantAwareDbContextFactory to preserve empty string for default tenant - Updated Store.cs to preserve empty string for default tenant - This ensures: null = tenant-agnostic (visible to all), "" = default tenant Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add database migration to convert null TenantId to empty string for SqlServer - Added Management migration to convert null to "" for WorkflowDefinitions and WorkflowInstances - Added Runtime migration to convert null to "" for all runtime entities - This ensures existing default tenant data is properly migrated per ADR-0008 - Note: Similar migrations needed for PostgreSql, MySql, Sqlite, and Oracle providers Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Clarify tenant handling logic in `ElsaDbContextBase` with new ADR references. * Add tenant ID convention analysis documents and quick reference guide * Implement tenant-agnostic functionality across modules - Introduce `AgnosticTenantId` constant to manage tenant-agnostic entities. - Modify entity handling logic to respect tenant-agnostic designations. - Adjust workflow processing to include tenant-agnostic workflows. - Update caching and activity descriptor logic to accommodate the `AgnosticTenantId`. * Refactor tenant management and registry logic in `ActivityRegistry` for improved clarity and separation of tenant-specific and tenant-agnostic activity descriptors. Remove `TestTenantResolver` and update workflow definition handling for tenant support. * Refactor `ActivityRegistry`: prioritize tenant-specific descriptors over tenant-agnostic and simplify descriptor retrieval logic. * Improve async handling in `CommandHandlerInvokerMiddleware` to await tasks without blocking * Update ADR to use asterisk as sentinel value for tenant-agnostic entities Replace the previous convention of using `null` for tenant-agnostic entities with an asterisk (`"*"`) for improved clarity and system architecture. Updated ADR documentation, TOC, and dependency graph accordingly. * Remove migration `ConvertNullTenantIdToEmptyString` and its associated designer file to clean up the codebase. * Refactor `ActivityRegistry`: streamline activity descriptor removal logic and simplify tenant ID checks. * Update Elsa.sln Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify `RefreshDescriptorsAsync` by removing unnecessary local variable `currentTenantId`. * Remove unused `currentTenantId` variable from `ActivityRegistry`. * Add detailed semantic flow and key points to ADR 0009 Document the tenant ID flow from entity creation to query, emphasizing normalization and tenant-agnostic workflows. Update semantic flow diagrams and provide testing considerations for preserving `"*"` values in multi-tenant scenarios. * Remove outdated Tenant ID Analysis and associated documents * Add security-by-default design for tenant-agnostic entities in ADR Enhance Architecture Decision Record to detail explicit requirements for tenant-agnostic database entities, highlighting differences between in-memory activity descriptors and persistent entities. Emphasize importance of setting `TenantId = "*"` to prevent accidental data leakage. * Normalize tenant ID grouping in `ActivityRegistry` to unify null and agnostic IDs, reducing redundant processing. * Refactor `SignalManager`: improve timeout handling and streamline signal task cancellation. * Update src/modules/Elsa.Workflows.Core/Models/TenantRegistryData.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Workflows.Core/Services/ActivityRegistry.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor tests to use `Tenant.AgnosticTenantId` instead of `null` for tenant-agnostic descriptors. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Enhance logging in recurring tasks: add error handling and logger support to prevent crashes in scheduled timers. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
186 lines
8.1 KiB
C#
186 lines
8.1 KiB
C#
using Elsa.Common;
|
|
using Elsa.Common.Multitenancy;
|
|
using Elsa.Expressions.Contracts;
|
|
using Elsa.Expressions.Services;
|
|
using Elsa.Extensions;
|
|
using Elsa.Mediator.Contracts;
|
|
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;
|
|
|
|
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();
|
|
return await ExecuteAsync(context);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes the activity using a pre-built <see cref="ActivityExecutionContext"/>.
|
|
/// Useful when you need to customize the context before execution, such as setting initial workflow state or overriding correlation IDs.
|
|
/// </summary>
|
|
/// <param name="context">The pre-built context to execute</param>
|
|
/// <returns>The <see cref="ActivityExecutionContext"/> after execution</returns>
|
|
public async Task<ActivityExecutionContext> ExecuteAsync(ActivityExecutionContext context)
|
|
{
|
|
// Set up variables and inputs, then execute the activity
|
|
await SetupExistingVariablesAsync(Activity, context);
|
|
await context.EvaluateInputPropertiesAsync();
|
|
context.TransitionTo(ActivityStatus.Running);
|
|
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 => typeof(Variable).IsAssignableFrom(p.PropertyType))
|
|
.ToList();
|
|
|
|
foreach (var variable in variableProperties.Select(property => (Variable?)property.GetValue(activity)))
|
|
{
|
|
if(variable == null)
|
|
continue;
|
|
|
|
context.WorkflowExecutionContext.MemoryRegister.Declare(variable);
|
|
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<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<IStimulusHasher, StimulusHasher>();
|
|
services.AddSingleton<ICommitStateHandler>(_ => Substitute.For<ICommitStateHandler>());
|
|
services.AddSingleton<IActivitySchedulerFactory, ActivitySchedulerFactory>();
|
|
services.AddSingleton<IWorkflowExecutionContextSchedulerStrategy, FakeWorkflowExecutionContextSchedulerStrategy>();
|
|
services.AddSingleton<IActivityExecutionContextSchedulerStrategy, FakeActivityExecutionContextSchedulerStrategy>();
|
|
services.AddSingleton<ITenantAccessor, DefaultTenantAccessor>();
|
|
}
|
|
} |