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