using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Contracts;
using Elsa.Features.Implementations;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Elsa.Features.UnitTests;
///
/// Tests , with an emphasis on features that introduce additional features from their own method.
///
public class ModuleTests
{
private const string AppliedFeaturesKey = "AppliedFeatures";
private readonly ServiceCollection _services = new();
private readonly Module _module;
private readonly List _appliedFeatures = new();
public ModuleTests()
{
_module = new(_services);
_module.Properties[AppliedFeaturesKey] = _appliedFeatures;
}
[Fact]
public void Apply_AppliesFeatureIntroducedFromApply()
{
_module.Configure();
_module.Apply();
Assert.Contains(typeof(IntroducedFeature), _appliedFeatures);
Assert.Contains(_services, x => x.ServiceType == typeof(IntroducedMarker));
}
[Fact]
public void Apply_AppliesEntireChainOfFeaturesIntroducedFromApply()
{
_module.Configure();
_module.Apply();
Assert.Contains(typeof(ChainMiddleFeature), _appliedFeatures);
Assert.Contains(typeof(ChainLeafFeature), _appliedFeatures);
}
[Fact]
public void Apply_AppliesDependenciesOfFeatureIntroducedFromApplyBeforeThatFeature()
{
_module.Configure();
_module.Apply();
Assert.Contains(typeof(IntroducedDependencyFeature), _appliedFeatures);
Assert.True(_appliedFeatures.IndexOf(typeof(IntroducedDependencyFeature)) < _appliedFeatures.IndexOf(typeof(IntroducedDependentFeature)));
}
[Fact]
public void Apply_RegistersHostedServicesOfFeatureIntroducedFromApply()
{
_module.Configure();
_module.Apply();
Assert.Contains(_services, x => x.ServiceType == typeof(IHostedService) && x.ImplementationType == typeof(IntroducedHostedService));
}
[Fact]
public void Apply_ListsFeatureIntroducedFromApplyInTheInstalledFeatureRegistry()
{
_module.Configure();
_module.Apply();
Assert.NotNull(GetInstalledFeatureRegistry().Find("Elsa.Introduced"));
}
[Fact]
public void Apply_AppliesEachFeatureOnlyOnce()
{
_module.Configure();
_module.Apply();
Assert.Equal(_appliedFeatures.Distinct().Count(), _appliedFeatures.Count);
}
[Fact]
public void Apply_AppliesFeaturesInDependencyOrder()
{
_module.Configure();
_module.Apply();
Assert.Equal([typeof(DependencyFeature), typeof(DependentFeature)], _appliedFeatures);
}
[Fact]
public void Apply_RegistersHostedServicesInPriorityOrder()
{
_module.ConfigureHostedService(2);
_module.ConfigureHostedService(1);
_module.Configure();
_module.Apply();
Assert.Equal([typeof(FirstHostedService), typeof(SecondHostedService), typeof(IntroducedHostedService)], GetHostedServiceTypes());
}
[Fact]
public void Apply_OrdersHostedServiceOfFeatureIntroducedFromApplyByPriority()
{
_module.ConfigureHostedService(10);
_module.Configure();
_module.Apply();
// The introduced feature configures its hosted service at priority 3, so it has to come first even though it shows up last.
Assert.Equal([typeof(IntroducedHostedService), typeof(SecondHostedService)], GetHostedServiceTypes());
}
[Fact]
public void Apply_RegistersConfiguredHostedServicesBeforeThoseRegisteredFromApply()
{
_module.ConfigureHostedService();
_module.Configure();
_module.Apply();
Assert.Equal([typeof(FirstHostedService), typeof(SelfRegisteredHostedService)], GetHostedServiceTypes());
}
private List GetHostedServiceTypes()
{
return _services.Where(x => x.ServiceType == typeof(IHostedService)).Select(x => x.ImplementationType).ToList();
}
private IInstalledFeatureRegistry GetInstalledFeatureRegistry()
{
return (IInstalledFeatureRegistry)_services.Single(x => x.ServiceType == typeof(IInstalledFeatureRegistry)).ImplementationInstance!;
}
///
/// Records the order in which features are applied so that tests can assert on it.
///
public abstract class RecordingFeature(IModule module) : FeatureBase(module)
{
public override void Apply() => ((List)Module.Properties[AppliedFeaturesKey]).Add(GetType());
}
public class IntroducingFeature(IModule module) : RecordingFeature(module)
{
public override void Apply()
{
base.Apply();
Module.Configure();
}
}
public class IntroducedFeature(IModule module) : RecordingFeature(module)
{
public override void ConfigureHostedServices() => ConfigureHostedService(3);
public override void Apply()
{
base.Apply();
Services.AddSingleton();
}
}
///
/// Registers a hosted service straight with the service collection, the way WorkflowRuntimeFeature does, rather than through the module.
///
public class HostedServiceRegisteringFeature(IModule module) : RecordingFeature(module)
{
public override void Apply()
{
base.Apply();
Services.AddHostedService();
}
}
public class ChainIntroducingFeature(IModule module) : RecordingFeature(module)
{
public override void Apply()
{
base.Apply();
Module.Configure();
}
}
public class ChainMiddleFeature(IModule module) : RecordingFeature(module)
{
public override void Apply()
{
base.Apply();
Module.Configure();
}
}
public class ChainLeafFeature(IModule module) : RecordingFeature(module);
public class IntroducingDependentFeature(IModule module) : RecordingFeature(module)
{
public override void Apply()
{
base.Apply();
Module.Configure();
}
}
[DependsOn(typeof(IntroducedDependencyFeature))]
public class IntroducedDependentFeature(IModule module) : RecordingFeature(module);
public class IntroducedDependencyFeature(IModule module) : RecordingFeature(module);
[DependsOn(typeof(DependencyFeature))]
public class DependentFeature(IModule module) : RecordingFeature(module);
public class DependencyFeature(IModule module) : RecordingFeature(module);
public class IntroducedMarker;
public abstract class NoopHostedService : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
public class IntroducedHostedService : NoopHostedService;
public class SelfRegisteredHostedService : NoopHostedService;
public class FirstHostedService : NoopHostedService;
public class SecondHostedService : NoopHostedService;
}