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;
///
/// 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.
///
public static class ActivityTestHelper
{
///
/// 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.
///
/// The activity to execute
/// An optional action to configure the service collection for dependency injection during activity execution.
/// The ActivityExecutionContext used for execution
public static async Task ExecuteActivityAsync(IActivity activity, Action? 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;
}
///
/// 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.
///
private static async Task CreateMinimalActivityExecutionContext(IActivity activity, Action? configureServices)
{
// Create a minimal service provider with the required services for expression evaluation
var services = new ServiceCollection();
// Add core services
services.AddLogging();
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton();
services.AddScoped();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
// Add the default expression descriptor provider which includes Literal expressions
services.AddSingleton();
services.AddSingleton();
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
services.AddSingleton(_ => Substitute.For());
// Call the configure services action if provided.
configureServices?.Invoke(services);
var serviceProvider = services.BuildServiceProvider();
var activityRegistry = serviceProvider.GetRequiredService();
var workflowGraphBuilder = serviceProvider.GetRequiredService();
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);
}
///
/// Sets up existing variables found on the activity in the execution context.
/// This is necessary because in unit tests, variables need to be initialized.
///
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;
}
///
/// 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.
///
/// A mock IResilientActivityInvoker configured to execute actions directly
public static IResilientActivityInvoker CreateMockResilientActivityInvoker()
{
var mock = Substitute.For();
// Configure the mock to simply execute the provided action directly
mock.InvokeAsync(
Arg.Any(),
Arg.Any(),
Arg.Any>>(),
Arg.Any())
.Returns(callInfo =>
{
var action = callInfo.ArgAt>>(2);
return action.Invoke();
});
return mock;
}
///
/// 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.
///
/// The service collection to add HTTP services to
public static void AddHttpServices(IServiceCollection services)
{
// Add all required HTTP services
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
// Add HTTP content parsers
AddHttpContentParsers(services);
// Add other required services
services.AddHttpClient();
}
///
/// Adds HTTP content parsers to the service collection.
/// These parsers are responsible for parsing different content types in HTTP responses.
///
/// The service collection to add parsers to
private static void AddHttpContentParsers(IServiceCollection services)
{
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
}
}