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(); } }