fix: preserve single latest draft when saving a new identity (#7918)
Co-authored-by: Shivamkmr8 <shivam.k@surya-fintech.com>
This commit is contained in:
parent
83af7309f2
commit
f50d6e984e
|
|
@ -221,25 +221,37 @@ public class WorkflowDefinitionPublisher(
|
|||
{
|
||||
DefinitionId = definitionId
|
||||
};
|
||||
var lastVersion = await workflowDefinitionStore.FindLastVersionAsync(filter, cancellationToken);
|
||||
var highestVersion = await workflowDefinitionStore.FindLastVersionAsync(filter, cancellationToken);
|
||||
var latestVersion = await workflowDefinitionStore.FindAsync(new()
|
||||
{
|
||||
DefinitionId = definitionId,
|
||||
VersionOptions = VersionOptions.Latest
|
||||
}, cancellationToken);
|
||||
|
||||
draft.Version = draft.Id == lastVersion?.Id ? lastVersion.Version : lastVersion?.Version + 1 ?? 1;
|
||||
draft.Version = draft.Id == latestVersion?.Id
|
||||
? latestVersion.Version
|
||||
: draft.Id == highestVersion?.Id
|
||||
? highestVersion.Version
|
||||
: highestVersion?.Version + 1 ?? 1;
|
||||
draft.IsLatest = true;
|
||||
draft = Initialize(draft);
|
||||
|
||||
await mediator.SendAsync(new WorkflowDefinitionDraftSaving(draft), cancellationToken);
|
||||
await workflowDefinitionStore.SaveAsync(draft, cancellationToken);
|
||||
|
||||
if (latestVersion != null && latestVersion.Id != draft.Id)
|
||||
{
|
||||
latestVersion.IsLatest = false;
|
||||
await workflowDefinitionStore.SaveManyAsync([latestVersion, draft], cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await workflowDefinitionStore.SaveAsync(draft, cancellationToken);
|
||||
}
|
||||
await mediator.SendAsync(new WorkflowDefinitionDraftSaved(draft), cancellationToken);
|
||||
|
||||
if (lastVersion is null)
|
||||
if (highestVersion is null)
|
||||
await mediator.SendAsync(new WorkflowDefinitionCreated(definition), cancellationToken);
|
||||
|
||||
if (lastVersion is { IsPublished: true, IsLatest: true })
|
||||
{
|
||||
lastVersion.IsLatest = false;
|
||||
await workflowDefinitionStore.SaveAsync(lastVersion, cancellationToken);
|
||||
}
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
using Elsa.Common.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows.Management;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.Management.Filters;
|
||||
using Elsa.Workflows.Management.Notifications;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowDefinitionVersioning;
|
||||
|
|
@ -15,12 +22,251 @@ public class Tests
|
|||
_services = new TestApplicationBuilder(testOutputHelper).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveDraftAsync_ShouldClearLatestFlagFromPreviousUnpublishedDraft()
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
var store = _services.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
var publisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
|
||||
var existingDraft = new WorkflowDefinition
|
||||
{
|
||||
Id = "v1",
|
||||
DefinitionId = definitionId,
|
||||
Version = 1,
|
||||
IsPublished = false,
|
||||
IsLatest = true
|
||||
};
|
||||
|
||||
var newDraft = new WorkflowDefinition
|
||||
{
|
||||
Id = "v2",
|
||||
DefinitionId = definitionId
|
||||
};
|
||||
|
||||
await store.SaveAsync(existingDraft);
|
||||
|
||||
var savedDraft = await publisher.SaveDraftAsync(newDraft);
|
||||
|
||||
var definitions = (await store.FindManyAsync(
|
||||
new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId
|
||||
}
|
||||
)).ToDictionary(x => x.Id);
|
||||
|
||||
Assert.False(definitions["v1"].IsLatest);
|
||||
Assert.True(definitions["v2"].IsLatest);
|
||||
Assert.Single(definitions.Values, x => x.IsLatest);
|
||||
Assert.Equal(2, definitions["v2"].Version);
|
||||
Assert.Equal(2, savedDraft.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveDraftAsync_ShouldKeepExistingDraftLatestWhenResaved()
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
var store = _services.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
var publisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
|
||||
var existingDraft = new WorkflowDefinition
|
||||
{
|
||||
Id = "draft-1",
|
||||
DefinitionId = definitionId,
|
||||
Version = 1,
|
||||
IsPublished = false,
|
||||
IsLatest = true
|
||||
};
|
||||
|
||||
await store.SaveAsync(existingDraft);
|
||||
|
||||
var savedDraft = await publisher.SaveDraftAsync(existingDraft);
|
||||
|
||||
var definitions = (await store.FindManyAsync(
|
||||
new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId
|
||||
}
|
||||
)).ToList();
|
||||
|
||||
var persistedDraft = Assert.Single(definitions);
|
||||
|
||||
Assert.Equal("draft-1", persistedDraft.Id);
|
||||
Assert.Equal(1, persistedDraft.Version);
|
||||
Assert.True(persistedDraft.IsLatest);
|
||||
Assert.Equal(1, savedDraft.Version);
|
||||
Assert.True(savedDraft.IsLatest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveDraftAsync_ShouldDemoteActualLatestWhenHighestVersionIsNotLatest()
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
var store = _services.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
var publisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
await store.SaveManyAsync([
|
||||
CreateDefinition("v1", definitionId, 1, true),
|
||||
CreateDefinition("v2", definitionId, 2, false)
|
||||
]);
|
||||
|
||||
var savedDraft = await publisher.SaveDraftAsync(CreateDefinition("v3", definitionId, 0, false));
|
||||
|
||||
var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter { DefinitionId = definitionId })).ToList();
|
||||
Assert.False(definitions.Single(x => x.Id == "v1").IsLatest);
|
||||
Assert.False(definitions.Single(x => x.Id == "v2").IsLatest);
|
||||
Assert.True(definitions.Single(x => x.Id == "v3").IsLatest);
|
||||
Assert.Equal(3, savedDraft.Version);
|
||||
Assert.Equal(3, definitions.Single(x => x.Id == "v3").Version);
|
||||
Assert.Single(definitions, x => x.IsLatest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveDraftAsync_ShouldPreserveActualLatestVersionWhenHigherVersionIsNotLatest()
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
var store = _services.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
var publisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
var latestDraft = CreateDefinition("v1", definitionId, 1, true);
|
||||
await store.SaveManyAsync([
|
||||
latestDraft,
|
||||
CreateDefinition("v2", definitionId, 2, false)
|
||||
]);
|
||||
|
||||
var savedDraft = await publisher.SaveDraftAsync(latestDraft);
|
||||
|
||||
var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter { DefinitionId = definitionId })).ToList();
|
||||
Assert.Equal(2, definitions.Count);
|
||||
Assert.Equal(1, savedDraft.Version);
|
||||
Assert.Equal(1, definitions.Single(x => x.Id == "v1").Version);
|
||||
Assert.True(definitions.Single(x => x.Id == "v1").IsLatest);
|
||||
Assert.False(definitions.Single(x => x.Id == "v2").IsLatest);
|
||||
Assert.Single(definitions, x => x.IsLatest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveDraftAsync_ShouldPreserveHighestVersionWhenPromotingNonLatestDraft()
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
var store = _services.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
var publisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
await store.SaveManyAsync([
|
||||
CreateDefinition("v1", definitionId, 1, true),
|
||||
CreateDefinition("v2", definitionId, 2, false)
|
||||
]);
|
||||
var highestDraft = await publisher.GetDraftAsync(definitionId, VersionOptions.SpecificVersion(2));
|
||||
|
||||
var savedDraft = await publisher.SaveDraftAsync(highestDraft!);
|
||||
|
||||
var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter { DefinitionId = definitionId })).ToList();
|
||||
Assert.Equal(2, definitions.Count);
|
||||
Assert.Equal("v2", savedDraft.Id);
|
||||
Assert.Equal(2, savedDraft.Version);
|
||||
Assert.False(definitions.Single(x => x.Id == "v1").IsLatest);
|
||||
Assert.True(definitions.Single(x => x.Id == "v2").IsLatest);
|
||||
Assert.Single(definitions, x => x.IsLatest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveDraftAsync_ShouldPreservePublishedStateWhenCreatingDraftAfterPublishedLatest()
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
var store = _services.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
var publisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
var publishedDefinition = CreateDefinition("v1", definitionId, 1, true);
|
||||
publishedDefinition.IsPublished = true;
|
||||
await store.SaveAsync(publishedDefinition);
|
||||
|
||||
var savedDraft = await publisher.SaveDraftAsync(CreateDefinition("v2", definitionId, 0, false));
|
||||
|
||||
var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter { DefinitionId = definitionId })).ToList();
|
||||
var persistedPublishedDefinition = definitions.Single(x => x.Id == "v1");
|
||||
Assert.True(persistedPublishedDefinition.IsPublished);
|
||||
Assert.False(persistedPublishedDefinition.IsLatest);
|
||||
Assert.Equal(1, persistedPublishedDefinition.Version);
|
||||
Assert.False(savedDraft.IsPublished);
|
||||
Assert.True(savedDraft.IsLatest);
|
||||
Assert.Equal(2, savedDraft.Version);
|
||||
Assert.Single(definitions, x => x.IsLatest);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(BatchFailurePoint.Replacement)]
|
||||
[InlineData(BatchFailurePoint.PreviousLatest)]
|
||||
public async Task SaveDraftAsync_WhenReplacementBatchFails_ShouldPreserveSinglePreviousLatest(
|
||||
BatchFailurePoint failurePoint
|
||||
)
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
|
||||
var persisted = new Dictionary<string, WorkflowDefinition>
|
||||
{
|
||||
["v1"] = CreateDefinition("v1", definitionId, 1, true)
|
||||
};
|
||||
|
||||
var store = CreateFailingStore(persisted, failurePoint);
|
||||
var publisher = ActivatorUtilities.CreateInstance<WorkflowDefinitionPublisher>(
|
||||
_services,
|
||||
store
|
||||
);
|
||||
|
||||
var replacement = CreateDefinition("v2", definitionId, 0, false);
|
||||
|
||||
await Assert.ThrowsAsync<TestPersistenceException>(
|
||||
() => publisher.SaveDraftAsync(replacement)
|
||||
);
|
||||
|
||||
var previous = Assert.Single(persisted.Values);
|
||||
|
||||
Assert.Equal("v1", previous.Id);
|
||||
Assert.True(previous.IsLatest);
|
||||
Assert.Single(persisted.Values, x => x.IsLatest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveDraftAsync_WhenDraftSavedNotificationFails_ShouldKeepCommittedLatestStateConsistent()
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
|
||||
var services = new TestApplicationBuilder(
|
||||
_services.GetRequiredService<ITestOutputHelper>()
|
||||
)
|
||||
.ConfigureServices(serviceCollection =>
|
||||
serviceCollection.AddNotificationHandler<
|
||||
FailingDraftSavedHandler,
|
||||
WorkflowDefinitionDraftSaved
|
||||
>()
|
||||
)
|
||||
.Build();
|
||||
|
||||
var store = services.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
var publisher = services.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
|
||||
await store.SaveAsync(CreateDefinition("v1", definitionId, 1, true));
|
||||
|
||||
await Assert.ThrowsAsync<TestNotificationException>(
|
||||
() => publisher.SaveDraftAsync(CreateDefinition("v2", definitionId, 0, false))
|
||||
);
|
||||
|
||||
var definitions = (await store.FindManyAsync(
|
||||
new WorkflowDefinitionFilter
|
||||
{
|
||||
DefinitionId = definitionId
|
||||
}
|
||||
)).ToList();
|
||||
|
||||
Assert.Equal(2, definitions.Count);
|
||||
Assert.False(definitions.Single(x => x.Id == "v1").IsLatest);
|
||||
Assert.True(definitions.Single(x => x.Id == "v2").IsLatest);
|
||||
Assert.Single(definitions, x => x.IsLatest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RevertVersionAsync_ShouldAllocateVersionAfterHighestStoredVersion()
|
||||
{
|
||||
const string definitionId = "test-definition";
|
||||
var store = _services.GetRequiredService<IWorkflowDefinitionStore>();
|
||||
var publisher = _services.GetRequiredService<IWorkflowDefinitionPublisher>();
|
||||
|
||||
var definitions = new WorkflowDefinition[]
|
||||
{
|
||||
new()
|
||||
|
|
@ -55,4 +301,128 @@ public class Tests
|
|||
|
||||
Assert.Equal(4, revertedDefinition.Version);
|
||||
}
|
||||
}
|
||||
|
||||
private static IWorkflowDefinitionStore CreateFailingStore(
|
||||
IDictionary<string, WorkflowDefinition> persisted,
|
||||
BatchFailurePoint failurePoint
|
||||
)
|
||||
{
|
||||
var store = Substitute.For<IWorkflowDefinitionStore>();
|
||||
|
||||
store.FindLastVersionAsync(
|
||||
Arg.Any<WorkflowDefinitionFilter>(),
|
||||
Arg.Any<CancellationToken>()
|
||||
)
|
||||
.Returns(_ =>
|
||||
Task.FromResult(
|
||||
persisted.Values
|
||||
.OrderByDescending(x => x.Version)
|
||||
.Select(Clone)
|
||||
.FirstOrDefault()
|
||||
)
|
||||
);
|
||||
|
||||
store.FindAsync(
|
||||
Arg.Is<WorkflowDefinitionFilter>(x => x.VersionOptions!.Value.IsLatest),
|
||||
Arg.Any<CancellationToken>()
|
||||
)
|
||||
.Returns(_ =>
|
||||
Task.FromResult(
|
||||
persisted.Values
|
||||
.Where(x => x.IsLatest)
|
||||
.Select(Clone)
|
||||
.FirstOrDefault()
|
||||
)
|
||||
);
|
||||
|
||||
store.SaveManyAsync(
|
||||
Arg.Any<IEnumerable<WorkflowDefinition>>(),
|
||||
Arg.Any<CancellationToken>()
|
||||
)
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var definitions = callInfo.Arg<IEnumerable<WorkflowDefinition>>().ToList();
|
||||
|
||||
var snapshot = persisted.ToDictionary(
|
||||
x => x.Key,
|
||||
x => Clone(x.Value)
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var definition in definitions)
|
||||
{
|
||||
if (
|
||||
failurePoint == BatchFailurePoint.PreviousLatest
|
||||
&& definition.Id == "v1"
|
||||
)
|
||||
{
|
||||
throw new TestPersistenceException();
|
||||
}
|
||||
|
||||
if (
|
||||
failurePoint == BatchFailurePoint.Replacement
|
||||
&& definition.Id == "v2"
|
||||
)
|
||||
{
|
||||
throw new TestPersistenceException();
|
||||
}
|
||||
|
||||
persisted[definition.Id] = Clone(definition);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
persisted.Clear();
|
||||
|
||||
foreach (var item in snapshot)
|
||||
{
|
||||
persisted[item.Key] = item.Value;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
private static WorkflowDefinition CreateDefinition(
|
||||
string id,
|
||||
string definitionId,
|
||||
int version,
|
||||
bool isLatest
|
||||
) =>
|
||||
new()
|
||||
{
|
||||
Id = id,
|
||||
DefinitionId = definitionId,
|
||||
Version = version,
|
||||
IsLatest = isLatest,
|
||||
IsPublished = false
|
||||
};
|
||||
|
||||
private static WorkflowDefinition Clone(WorkflowDefinition definition) =>
|
||||
definition.ShallowClone();
|
||||
|
||||
public enum BatchFailurePoint
|
||||
{
|
||||
Replacement,
|
||||
PreviousLatest
|
||||
}
|
||||
|
||||
private class FailingDraftSavedHandler
|
||||
: INotificationHandler<WorkflowDefinitionDraftSaved>
|
||||
{
|
||||
public Task HandleAsync(
|
||||
WorkflowDefinitionDraftSaved notification,
|
||||
CancellationToken cancellationToken
|
||||
) => throw new TestNotificationException();
|
||||
}
|
||||
|
||||
private class TestPersistenceException : Exception;
|
||||
|
||||
private class TestNotificationException : Exception;
|
||||
}
|
||||
Loading…
Reference in a new issue