diff --git a/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs b/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs index 794c9f91f..02c80c835 100644 --- a/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs +++ b/src/core/Elsa.Core/Services/ActivityExecutionContextForActivityBlueprintFactory.cs @@ -14,7 +14,6 @@ namespace Elsa.Services public ActivityExecutionContextForActivityBlueprintFactory(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - } /// diff --git a/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs b/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs index d397f2775..ab59f9abb 100644 --- a/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs +++ b/src/core/Elsa.Core/Services/StartActivitiesForCompositeActivityBlueprintProvider.cs @@ -16,16 +16,32 @@ namespace Elsa.Services /// A collection of the blueprint's starting activities public IEnumerable GetStartActivities(ICompositeActivityBlueprint compositeActivityBlueprint) { - var targetActivityIds = compositeActivityBlueprint.Connections - .Select(x => x.Target.Activity?.Id) - .Distinct() - .ToLookup(x => x); + var activityIdsThatAreNotStartingActivities = GetAllActivityIdsWhichHaveInboundConnections(compositeActivityBlueprint); var query = from activity in compositeActivityBlueprint.Activities - where !targetActivityIds.Contains(activity.Id) + where !activityIdsThatAreNotStartingActivities.Contains(activity.Id) select activity; return query; } + + /// + /// This method gets activities that have inbound connections. + /// + /// + /// + /// "Start activities" are those with no inbound connections; IE no workflow connection-target will point to a start activity. + /// What this method returns is essentially a blacklist of activity IDs which are not starting activities. + /// + /// + /// A composite activity blueprint + /// A lookup of activity IDs which are not starting activities + ILookup GetAllActivityIdsWhichHaveInboundConnections(ICompositeActivityBlueprint compositeActivityBlueprint) + { + return compositeActivityBlueprint.Connections + .Select(x => x.Target.Activity?.Id) + .Distinct() + .ToLookup(x => x); + } } } \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs b/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs new file mode 100644 index 000000000..8949c53a0 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/ActivityExecutionContextForActivityBlueprintFactoryTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Xunit; + +namespace Elsa.Services +{ + public class ActivityExecutionContextForActivityBlueprintFactoryTests + { + [Theory(DisplayName = "The CreateActivityExecutionContext method should create a context using the activity blueprint, the workflow execution context, cancellation token and injected service provider."), AutoMoqData] + public void CreateActivityExecutionContextCreatesContextUsingBlueprintExecutionContextCancellationTokenAndServiceProvider([AutofixtureServiceProvider] IServiceProvider serviceProvider, + IActivityBlueprint activityBlueprint, + [OmitOnRecursion] WorkflowExecutionContext workflowExecutionContext, + CancellationToken cancellationToken) + { + var sut = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); + + var result = sut.CreateActivityExecutionContext(activityBlueprint, workflowExecutionContext, cancellationToken); + + Assert.True(ReferenceEquals(activityBlueprint, result.ActivityBlueprint), "The activity blueprint should be the same"); + Assert.True(ReferenceEquals(workflowExecutionContext, result.WorkflowExecutionContext), "The workflow execution context should be the same"); + Assert.True(Equals(cancellationToken, result.CancellationToken), "The cancellation token should be equal"); + Assert.True(ReferenceEquals(serviceProvider, result.ServiceProvider), "The service provider should be the same"); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs b/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs new file mode 100644 index 000000000..eb421affa --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/StartActivitiesForCompositeActivityBlueprintProviderTests.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using Elsa.Services.Models; +using Moq; +using Xunit; + +namespace Elsa.Services +{ + public class StartActivitiesForCompositeActivityBlueprintProviderTests + { + [Theory(DisplayName = "The GetStartActivities returns only activities that have no inbound workflow connections"), AutoMoqData] + public void GetStartActivitiesReturnsAllActivitiesWhichHaveNoInboundConnections(StartActivitiesForCompositeActivityBlueprintProvider sut, + IWorkflowBlueprint workflowBlueprint, + IActivityBlueprint activityBlueprint1, + IActivityBlueprint activityBlueprint2, + IActivityBlueprint activityBlueprint3, + IActivityBlueprint activityBlueprint4, + string activityBlueprintId1, + string activityBlueprintId2, + string activityBlueprintId3, + string activityBlueprintId4, + IConnection connection1, + IConnection connection2, + ITargetEndpoint endpoint1, + ITargetEndpoint endpoint2) + { + SetupActivitiesWithIds(new () { + {activityBlueprintId1, activityBlueprint1}, + {activityBlueprintId2, activityBlueprint2}, + {activityBlueprintId3, activityBlueprint3}, + {activityBlueprintId4, activityBlueprint4}, + }); + Mock.Get(workflowBlueprint).SetupGet(x => x.Connections).Returns(new [] { connection1, connection2 }); + Mock.Get(workflowBlueprint).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint1, activityBlueprint2, activityBlueprint3, activityBlueprint4 }); + Mock.Get(connection1).SetupGet(x => x.Target).Returns(endpoint1); + Mock.Get(connection2).SetupGet(x => x.Target).Returns(endpoint2); + Mock.Get(endpoint1).SetupGet(x => x.Activity).Returns(activityBlueprint2); + Mock.Get(endpoint2).SetupGet(x => x.Activity).Returns(activityBlueprint4); + + var result = sut.GetStartActivities(workflowBlueprint).ToArray(); + + Assert.Equal(new [] { activityBlueprint1, activityBlueprint3 }, result); + } + + void SetupActivitiesWithIds(Dictionary idsToBlueprints) + { + foreach(var kvp in idsToBlueprints) + Mock.Get(kvp.Value).SetupGet(x => x.Id).Returns(kvp.Key); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs b/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs new file mode 100644 index 000000000..20ba00b63 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Services/WorkflowExecutionContextForWorkflowBlueprintFactoryTests.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using Elsa.Models; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Moq; +using Xunit; + +namespace Elsa.Services +{ + public class WorkflowExecutionContextForWorkflowBlueprintFactoryTests + { + [Theory(DisplayName = "The CreateWorkflowExecutionContextAsync method returns an execution context using the blueprint, an instance and the service provider"), AutoMoqData] + public async Task CreateWorkflowExecutionContextAsyncReturnsContextWithBlueprintInstanceAndServiceProvider([AutofixtureServiceProvider] IServiceProvider serviceProvider, + IWorkflowFactory workflowFactory, + IWorkflowBlueprint workflowBlueprint, + [OmitOnRecursion] WorkflowInstance instance) + { + var sut = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); + Mock.Get(workflowFactory) + .Setup(x => x.InstantiateAsync(workflowBlueprint, default, default, default)) + .Returns(() => Task.FromResult(instance)); + + var result = await sut.CreateWorkflowExecutionContextAsync(workflowBlueprint); + + Assert.Same(serviceProvider, result.ServiceProvider); + Assert.Same(workflowBlueprint, result.WorkflowBlueprint); + Assert.Same(instance, result.WorkflowInstance); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs new file mode 100644 index 000000000..22a0c1d69 --- /dev/null +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForActivityBlueprintAndWorkflowProviderTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elsa.ActivityProviders; +using Elsa.Bookmarks; +using Elsa.Models; +using Elsa.Services; +using Elsa.Services.Models; +using Elsa.Testing.Shared.AutoFixture.Attributes; +using Moq; +using Xunit; + +namespace Elsa.Triggers +{ + public class TriggersForActivityBlueprintAndWorkflowProviderTests + { + [Theory(DisplayName = "The GetTriggersForActivityBlueprintAsync returns a trigger for every bookmark in the bookmark providers that support the activity"), AutoMoqData] + public async Task GetTriggersForActivityBlueprintAsyncReturnsTriggersForEachBookmarkInSupportedBookmarkProviders(IBookmarkHasher bookmarkHasher, + IBookmarkProvider bookmarkProvider1, + IBookmarkProvider unsupportedBookmarkProvider, + IBookmarkProvider bookmarkProvider2, + ICreatesActivityExecutionContextForActivityBlueprint activityExecutionContextFactory, + IActivityBlueprint activityBlueprint, + [AutofixtureServiceProvider] IServiceProvider serviceProvider, + IWorkflowBlueprint workflowBlueprint, + [OmitOnRecursion] WorkflowInstance workflowInstance, + ActivityType activityType, + IBookmark bookmark1, + IBookmark bookmark2, + IBookmark bookmark3, + IBookmark bookmark4) + { + var sut = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, + new[] { bookmarkProvider1, unsupportedBookmarkProvider, bookmarkProvider2 }, + activityExecutionContextFactory); + var workflowExecutionContext = new WorkflowExecutionContext(serviceProvider, workflowBlueprint, workflowInstance); + var activityExecutionContext = new ActivityExecutionContext(serviceProvider, workflowExecutionContext, activityBlueprint, default, default, default); + + Mock.Get(activityExecutionContextFactory) + .Setup(x => x.CreateActivityExecutionContext(activityBlueprint, workflowExecutionContext, default)) + .Returns(activityExecutionContext); + Mock.Get(activityBlueprint).SetupGet(x => x.Type).Returns(activityType.TypeName); + Mock.Get(bookmarkProvider1) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(true)); + Mock.Get(unsupportedBookmarkProvider) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(false)); + Mock.Get(bookmarkProvider2) + .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) + .Returns(() => ValueTask.FromResult(true)); + Mock.Get(bookmarkProvider1) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(() => ValueTask.FromResult>(new [] { bookmark1, bookmark2 })); + Mock.Get(bookmarkProvider2) + .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) + .Returns(() => ValueTask.FromResult>(new [] { bookmark3, bookmark4 })); + + var result = await sut.GetTriggersForActivityBlueprintAsync(activityBlueprint, + workflowExecutionContext, + new Dictionary { { activityType.TypeName, activityType } }); + + Assert.True(result.Any(x => x.Bookmark == bookmark1), "Result contains a trigger for bookmark 1"); + Assert.True(result.Any(x => x.Bookmark == bookmark2), "Result contains a trigger for bookmark 2"); + Assert.True(result.Any(x => x.Bookmark == bookmark3), "Result contains a trigger for bookmark 3"); + Assert.True(result.Any(x => x.Bookmark == bookmark4), "Result contains a trigger for bookmark 4"); + Assert.True(result.Count() == 4, "Result has 4 items"); + } + } +} \ No newline at end of file diff --git a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs index b117cf20e..4ed32364e 100644 --- a/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs +++ b/test/unit/Elsa.UnitTests/Triggers/TriggersForBlueprintsProviderTests.cs @@ -18,134 +18,6 @@ namespace Elsa.UnitTests.Triggers { public class TriggersForBlueprintsProviderTests { - // [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for the compatible bookmarks of the start activities of the blueprints. This is actually an integration test."), AutoMoqData] - // public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks(IActivityTypeService activityTypeService, - // IBookmarkHasher bookmarkHasher, - // IBookmarkProvider provider1, - // IBookmarkProvider provider2, - // IBookmarkProvider provider3, - // [AutofixtureServiceProvider] IServiceProvider serviceProvider, - // IWorkflowFactory workflowFactory, - // ActivityType activityType1, - // ActivityType activityType2, - // ActivityType activityType3, - // IWorkflowBlueprint workflowBlueprint1, - // IWorkflowBlueprint workflowBlueprint2, - // IActivityBlueprint activityBlueprint1, - // IActivityBlueprint activityBlueprint2, - // IActivityBlueprint activityBlueprint3, - // IActivityBlueprint activityBlueprint4, - // IActivityBlueprint activityBlueprint5, - // IActivityBlueprint activityBlueprint6, - // string activityBlueprintId1, - // string activityBlueprintId2, - // string activityBlueprintId3, - // string activityBlueprintId4, - // string activityBlueprintId5, - // string activityBlueprintId6, - // IConnection connection1, - // IConnection connection2, - // IConnection connection3, - // ITargetEndpoint endpoint1, - // ITargetEndpoint endpoint2, - // ITargetEndpoint endpoint3, - // [OmitOnRecursion] WorkflowInstance workflow1, - // [OmitOnRecursion] WorkflowInstance workflow2, - // IBookmark bookmark1, - // IBookmark bookmark2, - // IBookmark bookmark3, - // IBookmark bookmark4, - // IBookmark bookmark5, - // IBookmark bookmark6, - // string bookmarkHash1, - // string bookmarkHash2, - // string bookmarkHash3, - // string bookmarkHash4, - // string bookmarkHash5, - // string bookmarkHash6) - // { - // var workflowExecutionContextFactory = new WorkflowExecutionContextForWorkflowBlueprintFactory(serviceProvider, workflowFactory); - // var activityExecutionContextFactory = new ActivityExecutionContextForActivityBlueprintFactory(serviceProvider); - // var triggersForActivityProvider = new TriggersForActivityBlueprintAndWorkflowProvider(bookmarkHasher, - // new[] { provider1, provider2, provider3 }, - // activityExecutionContextFactory); - // var startingActivitiesProvider = new StartActivitiesForCompositeActivityBlueprintProvider(); - - // var sut = new TriggersForBlueprintsProvider(activityTypeService, - // workflowExecutionContextFactory, - // triggersForActivityProvider, - // startingActivitiesProvider); - - // Mock.Get(workflowBlueprint1).SetupGet(x => x.Connections).Returns(new [] { connection1 }); - // Mock.Get(workflowBlueprint2).SetupGet(x => x.Connections).Returns(new [] { connection2, connection3 }); - // Mock.Get(workflowBlueprint1).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint1, activityBlueprint2, activityBlueprint4 }); - // Mock.Get(workflowBlueprint2).SetupGet(x => x.Activities).Returns(new [] { activityBlueprint3, activityBlueprint5, activityBlueprint6 }); - // Mock.Get(workflowFactory) - // .Setup(x => x.InstantiateAsync(workflowBlueprint1, null, null, default)) - // .Returns(Task.FromResult(workflow1)); - // Mock.Get(workflowFactory) - // .Setup(x => x.InstantiateAsync(workflowBlueprint2, null, null, default)) - // .Returns(Task.FromResult(workflow2)); - // Mock.Get(connection1).SetupGet(x => x.Target).Returns(endpoint1); - // Mock.Get(connection2).SetupGet(x => x.Target).Returns(endpoint2); - // Mock.Get(connection3).SetupGet(x => x.Target).Returns(endpoint3); - // Mock.Get(endpoint1).SetupGet(x => x.Activity).Returns(activityBlueprint4); - // Mock.Get(endpoint2).SetupGet(x => x.Activity).Returns(activityBlueprint5); - // Mock.Get(endpoint3).SetupGet(x => x.Activity).Returns(activityBlueprint6); - // Mock.Get(activityBlueprint1).SetupGet(x => x.Type).Returns(activityType1.TypeName); - // Mock.Get(activityBlueprint2).SetupGet(x => x.Type).Returns(activityType2.TypeName); - // Mock.Get(activityBlueprint3).SetupGet(x => x.Type).Returns(activityType3.TypeName); - // Mock.Get(activityBlueprint1).SetupGet(x => x.Id).Returns(activityBlueprintId1); - // Mock.Get(activityBlueprint2).SetupGet(x => x.Id).Returns(activityBlueprintId2); - // Mock.Get(activityBlueprint3).SetupGet(x => x.Id).Returns(activityBlueprintId3); - // Mock.Get(activityBlueprint4).SetupGet(x => x.Id).Returns(activityBlueprintId4); - // Mock.Get(activityBlueprint5).SetupGet(x => x.Id).Returns(activityBlueprintId5); - // Mock.Get(activityBlueprint6).SetupGet(x => x.Id).Returns(activityBlueprintId6); - // SetupProviderToBeCompatibleWithActivityType(provider1, activityType1); - // SetupProviderToBeCompatibleWithActivityType(provider2, activityType2); - // SetupProviderToBeCompatibleWithActivityType(provider3, activityType3); - // Mock.Get(provider1) - // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - // .Returns(ValueTask.FromResult>(new [] { bookmark1, bookmark2 })); - // Mock.Get(provider2) - // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - // .Returns(ValueTask.FromResult>(new [] { bookmark3, bookmark4 })); - // Mock.Get(provider3) - // .Setup(x => x.GetBookmarksAsync(It.IsAny(), default)) - // .Returns(ValueTask.FromResult>(new [] { bookmark5, bookmark6 })); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark1)).Returns(bookmarkHash1); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark2)).Returns(bookmarkHash2); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark3)).Returns(bookmarkHash3); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark4)).Returns(bookmarkHash4); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark5)).Returns(bookmarkHash5); - // Mock.Get(bookmarkHasher).Setup(x => x.Hash(bookmark6)).Returns(bookmarkHash6); - - // var result = await sut.GetTriggersAsync(new [] { workflowBlueprint1, workflowBlueprint2 }); - - // Assert.Equal(6, result.Count()); - // Assert.True(ContainsTriggerWithHash(result, bookmarkHash1) - // && ContainsTriggerWithHash(result, bookmarkHash2) - // && ContainsTriggerWithHash(result, bookmarkHash3) - // && ContainsTriggerWithHash(result, bookmarkHash4) - // && ContainsTriggerWithHash(result, bookmarkHash5) - // && ContainsTriggerWithHash(result, bookmarkHash6), - // "The returned triggers should contain a trigger with every one of the six expected hashes"); - // } - - // void SetupProviderToBeCompatibleWithActivityType(IBookmarkProvider provider, ActivityType activityType) - // { - // Mock.Get(provider) - // .Setup(x => x.SupportsActivityAsync(It.IsAny(), default)) - // .Returns(ValueTask.FromResult(false)); - // Mock.Get(provider) - // .Setup(x => x.SupportsActivityAsync(It.Is(c => c.ActivityType == activityType), default)) - // .Returns(ValueTask.FromResult(true)); - // } - - // bool ContainsTriggerWithHash(IEnumerable triggers, string hash) - // => triggers.Any(x => Equals(x.BookmarkHash, hash)); - - [Theory(DisplayName = "The GetTriggersAsync method should return all triggers for start activities of the workflow blueprints"), AutoMoqData] public async Task GetTriggersAsyncGetsAllTriggersForAllBlueprintsStartActivitiesAndCompatibleBookmarks([Frozen] IActivityTypeService activityTypeService, [Frozen] ICreatesWorkflowExecutionContextForWorkflowBlueprint workflowExecutionContextFactory,