From c1d0e7a0a2a544871aed4e3c7cc5bfd8f2577eaa Mon Sep 17 00:00:00 2001 From: Raymond den Haan Date: Thu, 14 Mar 2024 14:45:43 +0100 Subject: [PATCH] Made loading workflows more robust --- Elsa.sln | 7 + ...DefaultWorkflowDefinitionStorePopulator.cs | 121 +++++---- .../Elsa.Workflows.Runtime.UnitTests.csproj | 15 ++ ...ltWorkflowDefinitionStorePopulatorTests.cs | 234 ++++++++++++++++++ .../Usings.cs | 1 + ...ltWorkflowDefinitionStorePopulatorTests.cs | 234 ++++++++++++++++++ 6 files changed, 564 insertions(+), 48 deletions(-) create mode 100644 test/unit/Elsa.Workflows.Runtime.UnitTests/Elsa.Workflows.Runtime.UnitTests.csproj create mode 100644 test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultWorkflowDefinitionStorePopulatorTests.cs create mode 100644 test/unit/Elsa.Workflows.Runtime.UnitTests/Usings.cs create mode 100644 test/unit/Elsa.Workflows.Runtime.UnitTests1/Services/DefaultWorkflowDefinitionStorePopulatorTests.cs diff --git a/Elsa.sln b/Elsa.sln index 0d68b277f..35db33303 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -322,6 +322,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Hosting.Management", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.KeyValues", "src\modules\Elsa.KeyValues\Elsa.KeyValues.csproj", "{73BBB1E3-AEEC-44E8-83D7-6DD9CD687FE3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Workflows.Runtime.UnitTests", "test\unit\Elsa.Workflows.Runtime.UnitTests\Elsa.Workflows.Runtime.UnitTests.csproj", "{B0946844-DE0E-4C59-9391-C0EB649DBF4E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -786,6 +788,10 @@ Global {086A7E2A-6CE7-41DA-927B-C033638060B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {086A7E2A-6CE7-41DA-927B-C033638060B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {086A7E2A-6CE7-41DA-927B-C033638060B1}.Release|Any CPU.Build.0 = Release|Any CPU + {B0946844-DE0E-4C59-9391-C0EB649DBF4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0946844-DE0E-4C59-9391-C0EB649DBF4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0946844-DE0E-4C59-9391-C0EB649DBF4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0946844-DE0E-4C59-9391-C0EB649DBF4E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -925,6 +931,7 @@ Global {BBCE36D1-6767-4ED1-B3E8-84D2567A962A} = {A516931E-EDBB-4FC3-BB94-1BB824D5BC61} {73BBB1E3-AEEC-44E8-83D7-6DD9CD687FE3} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} {086A7E2A-6CE7-41DA-927B-C033638060B1} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5} + {B0946844-DE0E-4C59-9391-C0EB649DBF4E} = {18453B51-25EB-4317-A4B3-B10518252E92} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs index 193b7a4e7..3dc97a6be 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowDefinitionStorePopulator.cs @@ -92,6 +92,9 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP var workflow = materializedWorkflow.Workflow; var definitionId = workflow.Identity.DefinitionId; + var existingWorkflowLatest = false; + var existingWorkflowPublished = false; + // Serialize materializer context. var materializerContext = materializedWorkflow.MaterializerContext; var materializerContextJson = materializerContext != null ? _payloadSerializer.Serialize(materializerContext) : default; @@ -114,47 +117,8 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP if (existingDefinitionVersion != null) workflowDefinitionsToSave.Add(existingDefinitionVersion); - // If the workflow being added is configured to be the latest version, then we need to reset the current latest version. - if (workflow.Publication.IsLatest) - { - // Reset current latest definitions. - var filter = new WorkflowDefinitionFilter - { - DefinitionId = definitionId, - VersionOptions = VersionOptions.Latest - }; - var latestWorkflowDefinitions = (await _workflowDefinitionStore.FindManyAsync(filter, cancellationToken)).ToList(); - - // If the latest definitions contains definitions with the same ID then we need to replace them with the latest workflow definitions. - SyncExistingCopies(latestWorkflowDefinitions, workflowDefinitionsToSave); - - foreach (var latestWorkflowDefinition in latestWorkflowDefinitions) - { - latestWorkflowDefinition.IsLatest = false; - workflowDefinitionsToSave.Add(latestWorkflowDefinition); - } - } - - // If the workflow being added is configured to be the published version, then we need to reset the current published version. - if (workflow.Publication.IsPublished) - { - // Reset current published definitions. - var filter = new WorkflowDefinitionFilter - { - DefinitionId = definitionId, - VersionOptions = VersionOptions.Published - }; - var publishedWorkflowDefinitions = (await _workflowDefinitionStore.FindManyAsync(filter, cancellationToken)).ToList(); - - // If the published workflow definitions contains definitions with the same ID as definitions in the latest workflow definitions, then we need to replace them with the latest workflow definitions. - SyncExistingCopies(publishedWorkflowDefinitions, workflowDefinitionsToSave); - - foreach (var publishedWorkflowDefinition in publishedWorkflowDefinitions) - { - publishedWorkflowDefinition.IsPublished = false; - workflowDefinitionsToSave.Add(publishedWorkflowDefinition); - } - } + await UpdateIsLatest(); + await UpdateIsPublished(); var workflowDefinition = existingDefinitionVersion ?? new WorkflowDefinition { @@ -166,8 +130,8 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP workflowDefinition.Description = workflow.WorkflowMetadata.Description; workflowDefinition.Name = workflow.WorkflowMetadata.Name; workflowDefinition.ToolVersion = workflow.WorkflowMetadata.ToolVersion; - workflowDefinition.IsLatest = workflow.Publication.IsLatest; - workflowDefinition.IsPublished = workflow.Publication.IsPublished; + workflowDefinition.IsLatest = !existingWorkflowLatest; + workflowDefinition.IsPublished = !existingWorkflowPublished && workflow.Publication.IsPublished; workflowDefinition.IsReadonly = workflow.IsReadonly; workflowDefinition.IsSystem = workflow.IsSystem; workflowDefinition.CustomProperties = workflow.CustomProperties; @@ -182,15 +146,15 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP workflowDefinition.MaterializerContext = materializerContextJson; workflowDefinition.MaterializerName = materializedWorkflow.MaterializerName; - // Temporary measure to try and find the root cause of https://github.com/elsa-workflows/elsa-core/issues/5033 if (existingDefinitionVersion is null && workflowDefinitionsToSave.Any(w => w.Id == workflowDefinition.Id)) - _logger.LogError("Trying to create duplicate workflows with id {WorkflowDefinitionId}", workflowDefinition.Id); - else { - workflowDefinitionsToSave.Add(workflowDefinition); + _logger.LogError("Trying to create a new workflow with existing id {workflowId}", workflowDefinition.Id); + return; } - + + workflowDefinitionsToSave.Add(workflowDefinition); + var duplicates = workflowDefinitionsToSave.GroupBy(wd => wd.Id) .Where(g => g.Count() > 1) .Select(g => g.Key) @@ -202,6 +166,67 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP } await _workflowDefinitionStore.SaveManyAsync(workflowDefinitionsToSave, cancellationToken); + return; + + async Task UpdateIsLatest() + { + // Always try to update the IsLatest property based on the VersionNumber + + // Reset current latest definitions. + var filter = new WorkflowDefinitionFilter + { + DefinitionId = definitionId, + VersionOptions = VersionOptions.Latest + }; + var latestWorkflowDefinitions = (await _workflowDefinitionStore.FindManyAsync(filter, cancellationToken)).ToList(); + + // If the latest definitions contains definitions with the same ID then we need to replace them with the latest workflow definitions. + SyncExistingCopies(latestWorkflowDefinitions, workflowDefinitionsToSave); + + foreach (var latestWorkflowDefinition in latestWorkflowDefinitions) + { + if (latestWorkflowDefinition.Version > workflow.Identity.Version) + { + _logger.LogWarning("A more recent version of the workflow has been found, overwriting the IsLatest property on the workflow"); + existingWorkflowLatest = true; + continue; + } + + latestWorkflowDefinition.IsLatest = false; + workflowDefinitionsToSave.Add(latestWorkflowDefinition); + } + } + + async Task UpdateIsPublished() + { + // If the workflow being added is configured to be the published version, then we need to reset the current published version. + if (workflow.Publication.IsPublished) + { + // Reset current published definitions. + var filter = new WorkflowDefinitionFilter + { + DefinitionId = definitionId, + VersionOptions = VersionOptions.Published + }; + var publishedWorkflowDefinitions = (await _workflowDefinitionStore.FindManyAsync(filter, cancellationToken)).ToList(); + + // If the published workflow definitions contains definitions with the same ID as definitions in the latest workflow definitions, then we need to replace them with the latest workflow definitions. + SyncExistingCopies(publishedWorkflowDefinitions, workflowDefinitionsToSave); + + foreach (var publishedWorkflowDefinition in publishedWorkflowDefinitions) + { + if (publishedWorkflowDefinition.Version > workflow.Identity.Version) + { + _logger.LogWarning("A more recent version of the workflow has been found to be published, overwriting the IsPublished property on the workflow"); + existingWorkflowPublished = true; + continue; + } + + publishedWorkflowDefinition.IsPublished = false; + workflowDefinitionsToSave.Add(publishedWorkflowDefinition); + } + } + } } private async Task IndexTriggersAsync(MaterializedWorkflow workflow, CancellationToken cancellationToken) => await _triggerIndexer.IndexTriggersAsync(workflow.Workflow, cancellationToken); diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/Elsa.Workflows.Runtime.UnitTests.csproj b/test/unit/Elsa.Workflows.Runtime.UnitTests/Elsa.Workflows.Runtime.UnitTests.csproj new file mode 100644 index 000000000..4a25fd633 --- /dev/null +++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/Elsa.Workflows.Runtime.UnitTests.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + + + + + + + + + + + diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultWorkflowDefinitionStorePopulatorTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultWorkflowDefinitionStorePopulatorTests.cs new file mode 100644 index 000000000..e943aa154 --- /dev/null +++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/DefaultWorkflowDefinitionStorePopulatorTests.cs @@ -0,0 +1,234 @@ +using Elsa.Common.Contracts; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Models; +using Elsa.Workflows.Runtime.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; + +namespace Elsa.Workflows.Runtime.UnitTests.Services; + +public class DefaultWorkflowDefinitionStorePopulatorTests +{ + private readonly IWorkflowDefinitionStore _storeMock; + private readonly DefaultWorkflowDefinitionStorePopulator _populator; + private readonly List _workflowDefinitionsInStore = new(); + + public DefaultWorkflowDefinitionStorePopulatorTests() + { + _storeMock = Substitute.For(); + _storeMock.FindManyAsync(Arg.Any(), Arg.Any()) + .Returns(_workflowDefinitionsInStore); + _populator = new DefaultWorkflowDefinitionStorePopulator(() => new List(), + Substitute.For(), + _storeMock, + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For>()); + } + + [Fact(DisplayName = "When adding a new workflow it needs to be saved")] + public async Task AddOrUpdateCoreAsync_NewWorkflowDefinition_AddsWorkflowDefinition() + { + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 7, "1"), + Publication = new WorkflowPublication(true, true) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await CheckAmountOfWorkflowsSaved(1); + await CheckIfWorkflowWasSaved(workflow.Workflow.Identity, wd => wd.IsLatest); + } + + [Fact(DisplayName = "When adding a workflow with the same version, ignore it")] + public async Task AddOrUpdateCoreAsync_ExistingWorkflowDefinition_KeepsExistingWorkflow() + { + _workflowDefinitionsInStore.Add(new() + { + Id = "1", + DefinitionId = "a", + Version = 1, + Inputs = new List + { + new() + { + Name = "InputA" + } + } + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 1, "1"), + Inputs = new List + { + new() + { + Name = "A" + } + } + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await _storeMock.DidNotReceive().SaveManyAsync(Arg.Any>(), Arg.Any()); + } + + + [Theory(DisplayName = "When adding a newer workflow version, add it as latest and update the publication settings of older workflows")] + [InlineData(false, false, false, true)] + [InlineData(false, true, true, false)] + [InlineData(true, true, true, false)] + public async Task AddOrUpdateCoreAsync_NewWorkflowDefinitionVersion_AddsWorkflowDefinition(bool workflowAddedIsLatest, bool workflowAddedIsPublished, bool expectedPublishedStateAdded, bool expectedPublishedStateExisting) + { + _workflowDefinitionsInStore.Add(new() + { + Id = "1", + DefinitionId = "a", + Version = 1, + IsLatest = true, + IsPublished = true + }); + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 2, "2"), + Publication = new WorkflowPublication(workflowAddedIsLatest, workflowAddedIsPublished) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await CheckIfWorkflowWasSaved(wd => wd is { Id: "1", IsLatest: false } + && wd.IsPublished == expectedPublishedStateExisting); + await CheckIfWorkflowWasSaved(workflow.Workflow.Identity, wd => wd.IsLatest && wd.IsPublished == expectedPublishedStateAdded); + } + + [Fact(DisplayName = "When adding a newer workflow version, ignore the root Version number")] + public async Task AddOrUpdateCoreAsync_NewWorkflowDefinitionVersion_IgnoreRootVersion() + { + _workflowDefinitionsInStore.Add(new() + { + Id = "2", + DefinitionId = "a", + Version = 2, + IsLatest = true, + IsPublished = true + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 3, "1"), + Version = 1, + Publication = new WorkflowPublication(true, true) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await CheckIfWorkflowWasSaved(wd => wd is { Id: "2", IsLatest: false, IsPublished: false }); + await CheckIfWorkflowWasSaved(workflow.Workflow.Identity, wd => wd is { IsLatest: true, IsPublished: true }); + } + + [Fact(DisplayName = "When adding a workflow version with the same ID, ignore it")] + public async Task AddOrUpdateCoreAsync_NewWorkflowDefinitionVersionWithSameId_IsIgnored() + { + _workflowDefinitionsInStore.Add( + new() + { + Id = "1", + DefinitionId = "a", + Version = 1, + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 2, "1") + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await _storeMock.DidNotReceive() + .SaveManyAsync(Arg.Any>(), Arg.Any()); + } + + [Fact(DisplayName = "When adding a older workflow version, keep the current latest as latest")] + public async Task AddOrUpdateCoreAsync_OlderVersionAsLatest_ExistingVersionShouldRemainLatest() + { + _workflowDefinitionsInStore.Add( + new() + { + Id = "2", + DefinitionId = "a", + Version = 2, + IsLatest = true + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 1, "1"), + Publication = new WorkflowPublication(true, true) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await _storeMock.DidNotReceive().SaveManyAsync(Arg.Is>(l => + l.Any(wd => wd.Id == "2")), Arg.Any()); + + await CheckIfWorkflowWasSaved(workflow.Workflow.Identity, wd => wd.IsLatest == false); + } + + [Fact(DisplayName = "When adding a newer workflow version, keep the current published version published")] + public async Task AddOrUpdateCoreAsync_OlderVersionAsPublished_ShouldNotBeUpdated() + { + _storeMock.FindManyAsync(Arg.Any(), Arg.Any()) + .Returns(new List + { + new() + { + Id = "2", + DefinitionId = "a", + Version = 2, + IsPublished = true + } + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 1, "1"), + Publication = new WorkflowPublication(true, true) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + await _storeMock.DidNotReceive().SaveManyAsync(Arg.Is>(l => + l.Any(wd => wd.Id == "2")), Arg.Any()); + await _storeMock.Received().SaveManyAsync(Arg.Is>(l => + l.Any(wd => wd.Id == "1" && wd.IsPublished == false)), Arg.Any()); + } + + private async Task CheckIfWorkflowWasSaved(WorkflowIdentity identity, Func predicate) + { + await _storeMock.Received(1).SaveManyAsync(Arg.Is>(l => + l.Where(wd => wd.Id == identity.Id + && wd.DefinitionId == identity.DefinitionId + && wd.Version == identity.Version).Any(predicate)), Arg.Any()); + } + + private async Task CheckIfWorkflowWasSaved(Func predicate) + { + await _storeMock.Received(1).SaveManyAsync(Arg.Is>(l => + l.Any(predicate)), Arg.Any()); + } + + private async Task CheckAmountOfWorkflowsSaved(int count) + { + await _storeMock.Received(count) + .SaveManyAsync(Arg.Any>(), Arg.Any()); + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/Usings.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests1/Services/DefaultWorkflowDefinitionStorePopulatorTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests1/Services/DefaultWorkflowDefinitionStorePopulatorTests.cs new file mode 100644 index 000000000..e943aa154 --- /dev/null +++ b/test/unit/Elsa.Workflows.Runtime.UnitTests1/Services/DefaultWorkflowDefinitionStorePopulatorTests.cs @@ -0,0 +1,234 @@ +using Elsa.Common.Contracts; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Management.Contracts; +using Elsa.Workflows.Management.Entities; +using Elsa.Workflows.Management.Filters; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime.Contracts; +using Elsa.Workflows.Runtime.Models; +using Elsa.Workflows.Runtime.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; + +namespace Elsa.Workflows.Runtime.UnitTests.Services; + +public class DefaultWorkflowDefinitionStorePopulatorTests +{ + private readonly IWorkflowDefinitionStore _storeMock; + private readonly DefaultWorkflowDefinitionStorePopulator _populator; + private readonly List _workflowDefinitionsInStore = new(); + + public DefaultWorkflowDefinitionStorePopulatorTests() + { + _storeMock = Substitute.For(); + _storeMock.FindManyAsync(Arg.Any(), Arg.Any()) + .Returns(_workflowDefinitionsInStore); + _populator = new DefaultWorkflowDefinitionStorePopulator(() => new List(), + Substitute.For(), + _storeMock, + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For>()); + } + + [Fact(DisplayName = "When adding a new workflow it needs to be saved")] + public async Task AddOrUpdateCoreAsync_NewWorkflowDefinition_AddsWorkflowDefinition() + { + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 7, "1"), + Publication = new WorkflowPublication(true, true) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await CheckAmountOfWorkflowsSaved(1); + await CheckIfWorkflowWasSaved(workflow.Workflow.Identity, wd => wd.IsLatest); + } + + [Fact(DisplayName = "When adding a workflow with the same version, ignore it")] + public async Task AddOrUpdateCoreAsync_ExistingWorkflowDefinition_KeepsExistingWorkflow() + { + _workflowDefinitionsInStore.Add(new() + { + Id = "1", + DefinitionId = "a", + Version = 1, + Inputs = new List + { + new() + { + Name = "InputA" + } + } + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 1, "1"), + Inputs = new List + { + new() + { + Name = "A" + } + } + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await _storeMock.DidNotReceive().SaveManyAsync(Arg.Any>(), Arg.Any()); + } + + + [Theory(DisplayName = "When adding a newer workflow version, add it as latest and update the publication settings of older workflows")] + [InlineData(false, false, false, true)] + [InlineData(false, true, true, false)] + [InlineData(true, true, true, false)] + public async Task AddOrUpdateCoreAsync_NewWorkflowDefinitionVersion_AddsWorkflowDefinition(bool workflowAddedIsLatest, bool workflowAddedIsPublished, bool expectedPublishedStateAdded, bool expectedPublishedStateExisting) + { + _workflowDefinitionsInStore.Add(new() + { + Id = "1", + DefinitionId = "a", + Version = 1, + IsLatest = true, + IsPublished = true + }); + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 2, "2"), + Publication = new WorkflowPublication(workflowAddedIsLatest, workflowAddedIsPublished) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await CheckIfWorkflowWasSaved(wd => wd is { Id: "1", IsLatest: false } + && wd.IsPublished == expectedPublishedStateExisting); + await CheckIfWorkflowWasSaved(workflow.Workflow.Identity, wd => wd.IsLatest && wd.IsPublished == expectedPublishedStateAdded); + } + + [Fact(DisplayName = "When adding a newer workflow version, ignore the root Version number")] + public async Task AddOrUpdateCoreAsync_NewWorkflowDefinitionVersion_IgnoreRootVersion() + { + _workflowDefinitionsInStore.Add(new() + { + Id = "2", + DefinitionId = "a", + Version = 2, + IsLatest = true, + IsPublished = true + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 3, "1"), + Version = 1, + Publication = new WorkflowPublication(true, true) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await CheckIfWorkflowWasSaved(wd => wd is { Id: "2", IsLatest: false, IsPublished: false }); + await CheckIfWorkflowWasSaved(workflow.Workflow.Identity, wd => wd is { IsLatest: true, IsPublished: true }); + } + + [Fact(DisplayName = "When adding a workflow version with the same ID, ignore it")] + public async Task AddOrUpdateCoreAsync_NewWorkflowDefinitionVersionWithSameId_IsIgnored() + { + _workflowDefinitionsInStore.Add( + new() + { + Id = "1", + DefinitionId = "a", + Version = 1, + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 2, "1") + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await _storeMock.DidNotReceive() + .SaveManyAsync(Arg.Any>(), Arg.Any()); + } + + [Fact(DisplayName = "When adding a older workflow version, keep the current latest as latest")] + public async Task AddOrUpdateCoreAsync_OlderVersionAsLatest_ExistingVersionShouldRemainLatest() + { + _workflowDefinitionsInStore.Add( + new() + { + Id = "2", + DefinitionId = "a", + Version = 2, + IsLatest = true + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 1, "1"), + Publication = new WorkflowPublication(true, true) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + + await _storeMock.DidNotReceive().SaveManyAsync(Arg.Is>(l => + l.Any(wd => wd.Id == "2")), Arg.Any()); + + await CheckIfWorkflowWasSaved(workflow.Workflow.Identity, wd => wd.IsLatest == false); + } + + [Fact(DisplayName = "When adding a newer workflow version, keep the current published version published")] + public async Task AddOrUpdateCoreAsync_OlderVersionAsPublished_ShouldNotBeUpdated() + { + _storeMock.FindManyAsync(Arg.Any(), Arg.Any()) + .Returns(new List + { + new() + { + Id = "2", + DefinitionId = "a", + Version = 2, + IsPublished = true + } + }); + + var workflow = new MaterializedWorkflow(new Workflow + { + Identity = new WorkflowIdentity("a", 1, "1"), + Publication = new WorkflowPublication(true, true) + }, "Test", "Test"); + + await _populator.AddAsync(workflow); + await _storeMock.DidNotReceive().SaveManyAsync(Arg.Is>(l => + l.Any(wd => wd.Id == "2")), Arg.Any()); + await _storeMock.Received().SaveManyAsync(Arg.Is>(l => + l.Any(wd => wd.Id == "1" && wd.IsPublished == false)), Arg.Any()); + } + + private async Task CheckIfWorkflowWasSaved(WorkflowIdentity identity, Func predicate) + { + await _storeMock.Received(1).SaveManyAsync(Arg.Is>(l => + l.Where(wd => wd.Id == identity.Id + && wd.DefinitionId == identity.DefinitionId + && wd.Version == identity.Version).Any(predicate)), Arg.Any()); + } + + private async Task CheckIfWorkflowWasSaved(Func predicate) + { + await _storeMock.Received(1).SaveManyAsync(Arg.Is>(l => + l.Any(predicate)), Arg.Any()); + } + + private async Task CheckAmountOfWorkflowsSaved(int count) + { + await _storeMock.Received(count) + .SaveManyAsync(Arg.Any>(), Arg.Any()); + } +} \ No newline at end of file