Add project files.

This commit is contained in:
2025-08-18 17:11:43 +03:30
parent 807dc263be
commit e5eb10d604
3943 changed files with 223719 additions and 0 deletions
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\..\src\modules\Elsa.Common\Elsa.Common.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,29 @@
using Elsa.Extensions;
using Xunit;
namespace Elsa.Common.Core;
public class EnumerableExtensionsTests
{
[Fact]
public void ToBatches_ReturnsItemsInListOfPages()
{
var items = new List<int>
{
1,
2,
3,
4,
5
};
var batchSize = 2;
var batches = items.ToBatches(batchSize).ToList();
Assert.Equal(3, batches.Count);
Assert.Equal(2, batches.ElementAt(0).Count());
Assert.Equal(2, batches.ElementAt(1).Count());
Assert.Equal(1, batches.ElementAt(2).Count());
Assert.Equal(3, batches.ElementAt(1).ElementAt(0));
Assert.Equal(4, batches.ElementAt(1).ElementAt(1));
}
}
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\modules\Elsa.MongoDb\Elsa.MongoDb.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,38 @@
using Elsa.Common.Multitenancy;
using Elsa.KeyValues.Entities;
using Elsa.MongoDb.Common;
using Elsa.MongoDb.Modules.Runtime;
using MongoDB.Driver;
using NSubstitute;
namespace Elsa.MongoDb.UnitTests;
public class MongoKeyValueStoreTests
{
private readonly MongoDbStore<SerializedKeyValuePair> _mongoDbStore;
public MongoKeyValueStoreTests()
{
var mongoCollectionMock = Substitute.For<IMongoCollection<SerializedKeyValuePair>>();
var tenantAccessorMock = Substitute.For<ITenantAccessor>();
mongoCollectionMock.FindOneAndReplaceAsync(
Arg.Any<FilterDefinition<SerializedKeyValuePair>>(),
Arg.Any<SerializedKeyValuePair>(),
Arg.Any<FindOneAndReplaceOptions<SerializedKeyValuePair>>()
)
.Returns(new SerializedKeyValuePair());
_mongoDbStore = new MongoDbStore<SerializedKeyValuePair>(mongoCollectionMock, tenantAccessorMock);
}
[Fact(DisplayName = "When saving a SerializedKeyValuePair document, don't throw an exception of missing ID property")]
public async Task SaveAsync_WithSerializedKeyValuePairDocument_DoesNotThrowException()
{
var mongoKeyValueStore = new MongoKeyValueStore(_mongoDbStore);
var keyValuePair = new SerializedKeyValuePair();
var exception = await Record.ExceptionAsync(
async () => await mongoKeyValueStore.SaveAsync(keyValuePair, default));
Assert.Null(exception);
}
}
@@ -0,0 +1 @@
global using Xunit;
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,107 @@
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Workflows.Memory;
namespace Elsa.Workflows.Core.UnitTests;
public class ExpressionExecutionContextExtensionsTests
{
[Fact]
public void GetVariable_ReturnsVariable_WhenVariableExists()
{
// Arrange
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
});
var context = new ExpressionExecutionContext(null!, memoryRegister);
// Act
var result = context.GetVariable<int>("test");
// Assert
Assert.Equal(5, result);
}
[Fact]
public void GetVariable_ReturnsNull_WhenVariableDoesNotExist()
{
// Arrange
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>());
var context = new ExpressionExecutionContext(null!, memoryRegister);
// Act
var result = context.GetVariable<string>("nonexistent");
// Assert
Assert.Null(result);
}
[Fact]
public void CreateVariable_ThrowsException_WhenVariableExists()
{
// Arrange
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
});
var context = new ExpressionExecutionContext(null!, memoryRegister);
// Act & Assert
Assert.Throws<Exception>(() => context.CreateVariable("test", 10));
}
[Fact]
public void CreateVariable_CreatesVariable_WhenVariableDoesNotExist()
{
// Arrange
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>());
var context = new ExpressionExecutionContext(null!, memoryRegister);
// Act
context.CreateVariable("newVariable", 10);
// Assert
var variable = context.GetVariable<int>("newVariable");
Assert.Equal(10, variable);
}
[Fact]
public void SetVariable_CreatesVariable_WhenVariableDoesNotExist()
{
// Arrange
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>());
var context = new ExpressionExecutionContext(null!, memoryRegister);
// Act
context.CreateVariable("newVariable", 10);
// Assert
var variable = context.GetVariable<int>("newVariable");
Assert.Equal(10, variable);
}
[Fact]
public void SetVariable_SetsValue_WhenVariableExists()
{
// Arrange
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) }
});
var context = new ExpressionExecutionContext(null!, memoryRegister);
// Act
context.SetVariable("test", 10);
// Assert
var updatedVariable = context.GetVariable<int>("test");
Assert.Equal(10, updatedVariable);
}
}
@@ -0,0 +1,32 @@
using Elsa.Workflows.Activities.Flowchart.Models;
namespace Elsa.Workflows.Core.UnitTests.Flowchart;
public static class FlowGraphExtensions
{
public static void ValidateOutboundConnections(this FlowGraph flowGraph, List<string> expected, IActivity activity)
{
Assert.Equal(expected, flowGraph.GetOutboundConnections(activity).Select(c => c.ToString()));
}
public static void ValidateForwardInboundConnections(this FlowGraph flowGraph, List<string> expected, IActivity activity)
{
Assert.Equal(expected, flowGraph.GetForwardInboundConnections(activity).Select(c => c.ToString()));
}
public static void ValidateBackwardConnection(this FlowGraph flowGraph, bool expectedBackward, bool expectedValid, Connection connection)
{
var actualBackward = flowGraph.IsBackwardConnection(connection, out var actualValid);
Assert.Equal(expectedBackward, actualBackward);
Assert.Equal(expectedValid, actualValid);
}
public static void ValidateDanglingActivity(this FlowGraph flowGraph, bool expected, Activity activity)
{
Assert.Equal(expected, flowGraph.IsDanglingActivity(activity));
}
public static void ValidateAncestorActivities(this FlowGraph flowGraph, List<Activity> expected, Activity activity)
{
Assert.Equal(expected, flowGraph.GetAncestorActivities(activity));
}
}
@@ -0,0 +1,386 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Models;
namespace Elsa.Workflows.Core.UnitTests.Flowchart;
public class FlowGraphTests
{
// Start
// ↓
// A ← W Y
// ↙ ↘ ↙
// B C ← X ← Z
// ↘ ↙
// D
// ↓
// End
[Fact]
public void InvalidDanglingActivitiesTest()
{
var start = new TestActivity("start");
var a = new TestActivity("a");
var b = new TestActivity("b");
var c = new TestActivity("c");
var d = new TestActivity("d");
var end = new TestActivity("end");
var w = new TestActivity("w");
var x = new TestActivity("x");
var y = new TestActivity("y");
var z = new TestActivity("z");
var connections = new List<Connection>
{
new(start, a),
new(a, b),
new(a, c),
new(b, d),
new(c, d),
new(d, end),
new(w, a), // invalid dangling connector
new(x, c), // invalid dangling connector
new(y, x), // invalid dangling connector
new(z, x), // invalid dangling connector
};
var flowGraph = new FlowGraph(connections, start);
// FlowGraph.GetForwardInboundConnections
flowGraph.ValidateForwardInboundConnections(["start->a"], a);
flowGraph.ValidateForwardInboundConnections(["a->b"], b);
flowGraph.ValidateForwardInboundConnections(["a->c"], c);
flowGraph.ValidateForwardInboundConnections(["b->d", "c->d"], d);
flowGraph.ValidateForwardInboundConnections(["d->end"], end);
flowGraph.ValidateForwardInboundConnections([], x);
// FlowGraph.GetOutboundConnections
flowGraph.ValidateOutboundConnections(["start->a"], start);
flowGraph.ValidateOutboundConnections(["a->b", "a->c"], a);
flowGraph.ValidateOutboundConnections(["b->d"], b);
flowGraph.ValidateOutboundConnections(["c->d"], c);
flowGraph.ValidateOutboundConnections(["d->end"], d);
flowGraph.ValidateOutboundConnections([], end);
flowGraph.ValidateOutboundConnections(["x->c"], x);
// FlowGraph.GetAncestorActivities
flowGraph.ValidateAncestorActivities([], start);
flowGraph.ValidateAncestorActivities([start], a);
flowGraph.ValidateAncestorActivities([a, start], b);
flowGraph.ValidateAncestorActivities([a, start], c);
flowGraph.ValidateAncestorActivities([b, c, a, start], d);
flowGraph.ValidateAncestorActivities([], x);
// FlowGraph IsDanglingActivity
flowGraph.ValidateDanglingActivity(false, start);
flowGraph.ValidateDanglingActivity(false, a);
flowGraph.ValidateDanglingActivity(false, b);
flowGraph.ValidateDanglingActivity(false, c);
flowGraph.ValidateDanglingActivity(false, d);
flowGraph.ValidateDanglingActivity(false, end);
flowGraph.ValidateDanglingActivity(true, w);
flowGraph.ValidateDanglingActivity(true, x);
flowGraph.ValidateDanglingActivity(true, y);
flowGraph.ValidateDanglingActivity(true, z);
}
// Start
// ↓
// A ← ←
// ↙ ↘ ↖
// B C ↑
// ↘ ↙ ↗
// D → →
[Fact]
public void ValidBackwardConnectionTest()
{
var start = new TestActivity("start");
var a = new TestActivity("a");
var b = new TestActivity("b");
var c = new TestActivity("c");
var d = new TestActivity("d");
var connections = new List<Connection>
{
new(start, a),
new(a, b),
new(a, c),
new(b, d),
new(c, d),
new(d, a), // loopback
};
var flowGraph = new FlowGraph(connections, start);
// FlowGraph.GetForwardInboundConnections
flowGraph.ValidateForwardInboundConnections([], start);
flowGraph.ValidateForwardInboundConnections(["start->a"], a);
flowGraph.ValidateForwardInboundConnections(["a->b"], b);
flowGraph.ValidateForwardInboundConnections(["a->c"], c);
flowGraph.ValidateForwardInboundConnections(["b->d", "c->d"], d);
// FlowGraph.GetOutboundConnections
flowGraph.ValidateOutboundConnections(["start->a"], start);
flowGraph.ValidateOutboundConnections(["a->b", "a->c"], a);
flowGraph.ValidateOutboundConnections(["b->d"], b);
flowGraph.ValidateOutboundConnections(["c->d"], c);
flowGraph.ValidateOutboundConnections(["d->a"], d);
// FlowGraph.GetAncestorActivities
flowGraph.ValidateAncestorActivities([], start);
flowGraph.ValidateAncestorActivities([start], a);
flowGraph.ValidateAncestorActivities([a, start], b);
flowGraph.ValidateAncestorActivities([a, start], c);
flowGraph.ValidateAncestorActivities([b, c, a, start], d);
// FlowGraph.IsBackwardConnection
flowGraph.ValidateBackwardConnection(false, false, new(a, c));
flowGraph.ValidateBackwardConnection(true, true, new(d, a));
}
// Start
// ↙ ↘
// A B ← ←
// ↘ ↙ ↘ ↖
// C D ↰ ↑
// ↘ ↙ ↗ ↗
// E → →
[Fact]
public void InvalidLoopbackTest()
{
var start = new TestActivity("start");
var a = new TestActivity("a");
var b = new TestActivity("b");
var c = new TestActivity("c");
var d = new TestActivity("d");
var e = new TestActivity("e");
var connections = new List<Connection>
{
new(start, a),
new(start, b),
new(a, c),
new(b, c),
new(b, d),
new(d, e),
new(c, e),
new(e, b), // invalid loopback
new(e, d), // invalid loopback
};
var flowGraph = new FlowGraph(connections, start);
// FlowGraph.GetForwardInboundConnections
flowGraph.ValidateForwardInboundConnections([], start);
flowGraph.ValidateForwardInboundConnections(["start->a"], a);
flowGraph.ValidateForwardInboundConnections(["start->b"], b);
flowGraph.ValidateForwardInboundConnections(["a->c", "b->c"], c);
flowGraph.ValidateForwardInboundConnections(["b->d"], d);
flowGraph.ValidateForwardInboundConnections(["c->e", "d->e"], e);
// FlowGraph.GetOutboundConnections
flowGraph.ValidateOutboundConnections(["start->a", "start->b"], start);
flowGraph.ValidateOutboundConnections(["a->c"], a);
flowGraph.ValidateOutboundConnections(["b->c", "b->d"], b);
flowGraph.ValidateOutboundConnections(["c->e"], c);
flowGraph.ValidateOutboundConnections(["d->e"], d);
flowGraph.ValidateOutboundConnections(["e->b", "e->d"], e);
// FlowGraph.GetAncestorActivities
flowGraph.ValidateAncestorActivities([], start);
flowGraph.ValidateAncestorActivities([start], a);
flowGraph.ValidateAncestorActivities([start], b);
flowGraph.ValidateAncestorActivities([a, b, start], c);
flowGraph.ValidateAncestorActivities([b, start], d);
flowGraph.ValidateAncestorActivities([c, d, a, b, start], e);
// FlowGraph.IsBackwardConnection
flowGraph.ValidateBackwardConnection(true, false, new(e, b));
flowGraph.ValidateBackwardConnection(true, false, new(e, d));
}
// Start
// ↙ ↘
// ↓ A
// ↓ ↓
// ↓ B
// ↓ ↙ ↓
// C D
// ↘ ↙
// End
[Fact]
public void LongLegTest()
{
var start = new TestActivity("start");
var a = new TestActivity("a");
var b = new TestActivity("b");
var c = new TestActivity("c");
var d = new TestActivity("d");
var end = new TestActivity("end");
var connections = new List<Connection>
{
new(start, a),
new(start, c),
new(a, b),
new(b, c),
new(b, d),
new(c, end),
new(d, end),
};
var flowGraph = new FlowGraph(connections, start);
// FlowGraph.GetForwardInboundConnections
flowGraph.ValidateForwardInboundConnections([], start);
flowGraph.ValidateForwardInboundConnections(["start->a"], a);
flowGraph.ValidateForwardInboundConnections(["a->b"], b);
flowGraph.ValidateForwardInboundConnections(["start->c", "b->c"], c);
flowGraph.ValidateForwardInboundConnections(["b->d"], d);
flowGraph.ValidateForwardInboundConnections(["c->end", "d->end"], end);
// FlowGraph.GetOutboundConnections
flowGraph.ValidateOutboundConnections(["start->a", "start->c"], start);
flowGraph.ValidateOutboundConnections(["a->b"], a);
flowGraph.ValidateOutboundConnections(["b->c", "b->d"], b);
flowGraph.ValidateOutboundConnections(["c->end"], c);
flowGraph.ValidateOutboundConnections(["d->end"], d);
flowGraph.ValidateOutboundConnections([], end);
// FlowGraph.GetAncestorActivities
flowGraph.ValidateAncestorActivities([], start);
flowGraph.ValidateAncestorActivities([start], a);
flowGraph.ValidateAncestorActivities([a, start], b);
flowGraph.ValidateAncestorActivities([start, b, a], c);
flowGraph.ValidateAncestorActivities([b, a, start], d);
flowGraph.ValidateAncestorActivities([c, d, start, b, a], end);
}
// Start
// ↙ ↘
// A B
// ↓↘ ↙ ↘
// ↳→C D
// ↓↘ ↙
// ↳→E
[Fact]
public void SameEdgeDuplicateTest()
{
var start = new TestActivity("start");
var a = new TestActivity("a");
var b = new TestActivity("b");
var c = new TestActivity("c");
var d = new TestActivity("d");
var e = new TestActivity("e");
var connections = new List<Connection>
{
new(start, a),
new(start, b),
new(a, c),
new(a, c), // duplicate
new(b, c),
new(b, d),
new(d, e),
new(c, e),
new(c, e), // duplicate
};
var flowGraph = new FlowGraph(connections, start);
// FlowGraph.GetForwardInboundConnections
flowGraph.ValidateForwardInboundConnections([], start);
flowGraph.ValidateForwardInboundConnections(["start->a"], a);
flowGraph.ValidateForwardInboundConnections(["start->b"], b);
flowGraph.ValidateForwardInboundConnections(["a->c", "b->c"], c);
flowGraph.ValidateForwardInboundConnections(["b->d"], d);
flowGraph.ValidateForwardInboundConnections(["c->e", "d->e"], e);
// FlowGraph.GetOutboundConnections
flowGraph.ValidateOutboundConnections(["start->a", "start->b"], start);
flowGraph.ValidateOutboundConnections(["a->c"], a);
flowGraph.ValidateOutboundConnections(["b->c", "b->d"], b);
flowGraph.ValidateOutboundConnections(["c->e"], c);
flowGraph.ValidateOutboundConnections(["d->e"], d);
flowGraph.ValidateOutboundConnections([], e);
// FlowGraph.GetAncestorActivities
flowGraph.ValidateAncestorActivities([], start);
flowGraph.ValidateAncestorActivities([start], a);
flowGraph.ValidateAncestorActivities([start], b);
flowGraph.ValidateAncestorActivities([a, b, start], c);
flowGraph.ValidateAncestorActivities([b, start], d);
flowGraph.ValidateAncestorActivities([c, d, a, b, start], e);
}
// Start
// ↙ ↘
// A B
// ↓↘ ↙ ↘
// ↳→C D
// ↓↘ ↙
// ↳→E
[Fact]
public void SameEdgeDifferentPortDuplicateTest()
{
var start = new TestActivity("start");
var a = new TestActivity("a");
var b = new TestActivity("b");
var c = new TestActivity("c");
var d = new TestActivity("d");
var e = new TestActivity("e");
var connections = new List<Connection>
{
new(start, a),
new(start, b),
new(start, b), // duplicate
new(a, c),
new(new Endpoint(a, "Yes"), new Endpoint(c)),
new(new Endpoint(a, "Yes"), new Endpoint(c)), // duplicate
new(b, c),
new(b, d),
new(c, e),
new(new Endpoint(c, "Yes"), new Endpoint(e)),
new(d, e),
};
var flowGraph = new FlowGraph(connections, start);
// FlowGraph.GetForwardInboundConnections
flowGraph.ValidateForwardInboundConnections([], start);
flowGraph.ValidateForwardInboundConnections(["start->a"], a);
flowGraph.ValidateForwardInboundConnections(["start->b"], b);
flowGraph.ValidateForwardInboundConnections(["a->c", "a:Yes->c", "b->c"], c);
flowGraph.ValidateForwardInboundConnections(["b->d"], d);
flowGraph.ValidateForwardInboundConnections(["c->e", "c:Yes->e", "d->e"], e);
// FlowGraph.GetOutboundConnections
flowGraph.ValidateOutboundConnections(["start->a", "start->b"], start);
flowGraph.ValidateOutboundConnections(["a->c", "a:Yes->c"], a);
flowGraph.ValidateOutboundConnections(["b->c", "b->d"], b);
flowGraph.ValidateOutboundConnections(["c->e", "c:Yes->e"], c);
flowGraph.ValidateOutboundConnections(["d->e"], d);
flowGraph.ValidateOutboundConnections([], e);
// FlowGraph.GetAncestorActivities
flowGraph.ValidateAncestorActivities([], start);
flowGraph.ValidateAncestorActivities([start], a);
flowGraph.ValidateAncestorActivities([start], b);
flowGraph.ValidateAncestorActivities([a, b, start], c);
flowGraph.ValidateAncestorActivities([b, start], d);
flowGraph.ValidateAncestorActivities([c, d, a, b, start], e);
}
class TestActivity : Activity
{
public TestActivity(string id)
{
Id = id;
Name = id;
}
public override string ToString()
{
return Id;
}
}
}
@@ -0,0 +1,9 @@
namespace Elsa.Workflows.Core.UnitTests.ObjectConversion
{
public class Person
{
public double? Age { get; set; }
public string? Name { get; set; }
}
}
@@ -0,0 +1,355 @@
using System.Dynamic;
using System.Text.Json;
using System.Text.Json.Nodes;
using Elsa.Expressions.Exceptions;
using Elsa.Expressions.Helpers;
namespace Elsa.Workflows.Core.UnitTests.ObjectConversion;
public class Tests
{
private readonly ObjectConverterOptions _objectConverterOptions = new(StrictMode: true);
[Fact]
public void TryConvertTo_SameType_ReturnsSuccess()
{
// Arrange
var value = 42;
// Act
var result = value.TryConvertTo<int>(_objectConverterOptions);
// Assert
Assert.True(result.Success);
Assert.Equal(42, result.Value);
}
[Fact]
public void TryConvertTo_DifferentType_ReturnsConvertedValue()
{
// Arrange
var value = "42";
// Act
var result = value.TryConvertTo<int>(_objectConverterOptions);
// Assert
Assert.True(result.Success);
Assert.Equal(42, result.Value);
}
[Fact]
public void TryConvertTo_InvalidConversion_ReturnsFailure()
{
// Arrange
var value = "invalid";
// Act
var result = value.TryConvertTo<int>(_objectConverterOptions);
// Assert
Assert.False(result.Success);
Assert.NotNull(result.Exception);
}
[Fact]
public void TryConvertTo_InvalidJsonString_ReturnsFailure()
{
// Arrange
var value = "{ invalid json }";
// Act
var result = value.TryConvertTo<Dictionary<string, object>>(_objectConverterOptions);
// Assert
Assert.False(result.Success);
Assert.NotNull(result.Exception);
}
[Fact]
public void ConvertTo_NullValue_ReturnsDefault()
{
// Arrange
object? value = null;
// Act
var result = value.ConvertTo<int>(_objectConverterOptions);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void ConvertTo_JsonElementNumberToString_ReturnsString()
{
// Arrange
var jsonElement = JsonNode.Parse("42")!.AsValue();
// Act
var result = jsonElement.ConvertTo<string>();
// Assert
Assert.Equal("42", result);
}
[Fact]
public void ConvertTo_JsonNodeToExpandoObject_ReturnsExpandoObject()
{
// Arrange
var jsonNode = JsonNode.Parse("{ \"key\": \"value\" }");
// Act
var result = jsonNode.ConvertTo<ExpandoObject>(_objectConverterOptions);
// Assert
dynamic expando = Assert.IsType<ExpandoObject>(result);
object value = expando.key;
// This is not the result I expect, I would have expected the JsonNode to have been recursively converted
Assert.True(value is JsonElement);
Assert.Equal(JsonValueKind.String, ((JsonElement)value).ValueKind);
Assert.Equal("value", ((JsonElement)value).GetString());
}
[Fact]
public void ConvertTo_StringToDateTime_ReturnsDateTime()
{
// Arrange
var value = "2023-01-01T00:00:00";
// Act
var result = value.ConvertTo<DateTime>(_objectConverterOptions);
// Assert
Assert.Equal(new(2023, 1, 1, 0, 0, 0), result);
}
[Fact]
public void ConvertTo_StringToEnum_ReturnsEnum()
{
// Arrange
var value = "Monday";
// Act
var result = value.ConvertTo<DayOfWeek>(_objectConverterOptions);
// Assert
Assert.Equal(DayOfWeek.Monday, result);
}
[Fact]
public void ConvertTo_StringToByteArray_ReturnsByteArray()
{
// Arrange
var value = Convert.ToBase64String(new byte[]
{
1, 2, 3
});
// Act
var result = value.ConvertTo<byte[]>(_objectConverterOptions);
// Assert
Assert.Equal(new byte[]
{
1, 2, 3
}, result);
}
[Fact]
public void ConvertTo_InvalidJsonString_ThrowsException()
{
// Arrange
var value = "{ invalid json }";
// Act & Assert
Assert.Throws<TypeConversionException>(() => value.ConvertTo<Dictionary<string, object>>(_objectConverterOptions));
}
[Fact]
public void ConvertTo_EnumerableToList_ReturnsConvertedList()
{
// Arrange
var value = new[]
{
"1", "2", "3"
};
// Act
var result = value.ConvertTo<List<int>>(_objectConverterOptions);
// Assert
Assert.Equal([
1,
2,
3
], result);
}
[Fact]
public void ConvertTo_DateTimeToDateOnly_ReturnsDateOnly()
{
// Arrange
var value = new DateTime(2023, 1, 1);
// Act
var result = value.ConvertTo<DateOnly>(_objectConverterOptions);
// Assert
Assert.Equal(new(2023, 1, 1), result);
}
[Fact]
public void ConvertTo_DateOnlyToDateTime_ReturnsDateTime()
{
// Arrange
var value = new DateOnly(2023, 1, 1);
// Act
var result = value.ConvertTo<DateTime>(_objectConverterOptions);
// Assert
Assert.Equal(new(2023, 1, 1, 0, 0, 0), result);
}
[Fact]
public void ConvertTo_UnknownConversion_ThrowsInvalidCastException()
{
// Arrange
var value = new object();
// Act & Assert
Assert.Throws<TypeConversionException>(() => value.ConvertTo<int>(_objectConverterOptions));
}
[Fact]
public void ConvertTo_JsonArrayToListOfObject_ReturnsListOfJsonObject()
{
// Arrange
var jsonArrayString = "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]";
var jsonArray = JsonNode.Parse(jsonArrayString);
// Act
var result = jsonArray.ConvertTo<List<object>>(_objectConverterOptions);
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
Assert.IsType<JsonObject>(result[0]);
Assert.IsType<JsonObject>(result[1]);
var firstElement = result[0] as JsonObject;
var secondElement = result[1] as JsonObject;
Assert.NotNull(firstElement);
Assert.Equal("Alice", firstElement["name"]?.ToString());
Assert.Equal("30", firstElement["age"]?.ToString());
Assert.NotNull(secondElement);
Assert.Equal("Bob", secondElement["name"]?.ToString());
Assert.Equal("25", secondElement["age"]?.ToString());
}
[Fact]
public void ConvertTo_JsonArrayToICollectionOfObject_ReturnsCollectionOfJsonObject()
{
// Arrange
var jsonArrayString = "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]";
var jsonArray = JsonNode.Parse(jsonArrayString);
// Act
var result = jsonArray.ConvertTo<ICollection<object>>(_objectConverterOptions);
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
Assert.All(result, item => Assert.IsType<JsonObject>(item));
var firstElement = result.First() as JsonObject;
var secondElement = result.Last() as JsonObject;
Assert.NotNull(firstElement);
Assert.Equal("Alice", firstElement["name"]?.ToString());
Assert.Equal("30", firstElement["age"]?.ToString());
Assert.NotNull(secondElement);
Assert.Equal("Bob", secondElement["name"]?.ToString());
Assert.Equal("25", secondElement["age"]?.ToString());
}
[Fact]
public void ConvertFrom_JsonArrayToArrayOfComplexType_ReturnsArrayOfComplexType()
{
// Arrange
var jsonArrayString = "[{\"name\":\"Alice\",\"age\":30},{\"name\":\"Bob\",\"age\":25}]";
// Act
var result = jsonArrayString.ConvertTo<Person[]>(_objectConverterOptions);
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Length);
Assert.Equal("Alice", result[0].Name);
Assert.Equal("Bob", result[1].Name);
}
[Fact]
public void ConvertFrom_ObjectArrayOfDoubleToArrayOfDouble_ReturnsArrayOfDouble()
{
// Arrange
object[] objectArray = [1d, 2d, 3d];
// Act
var result = objectArray.ConvertTo<double[]>(_objectConverterOptions);
// Assert
Assert.NotNull(result);
}
[Theory]
[InlineData("foo", false, typeof(bool))]
[InlineData("true", true, typeof(bool))]
[InlineData("false", false, typeof(bool))]
[InlineData("bar", 0, typeof(int))]
[InlineData("123", 123, typeof(int))]
[InlineData("notadate", null!, typeof(DateTime?))]
[InlineData("2023-01-01T00:00:00", "2023-01-01T00:00:00", typeof(DateTime))]
public void ConvertTo_StrictModeDisabled_ReturnsDefaultOrConverted(string input, object? expected, Type targetType)
{
var options = new ObjectConverterOptions(StrictMode: false);
var result = input.ConvertTo(targetType, options);
if (expected is null)
{
Assert.Null(result);
return;
}
// Special handling for DateTime
if (targetType == typeof(DateTime) && expected is string expectedString)
{
var expectedDateTime = DateTime.Parse(expectedString, null, System.Globalization.DateTimeStyles.RoundtripKind);
Assert.Equal(expectedDateTime, result);
return;
}
Assert.Equal(expected, result);
}
[Theory]
[InlineData("foo", true, typeof(bool))]
[InlineData("true", false, typeof(bool))]
[InlineData("false", false, typeof(bool))]
[InlineData("bar", true, typeof(int))]
[InlineData("123", false, typeof(int))]
[InlineData("notadate", true, typeof(DateTime?))]
[InlineData("2023-01-01T00:00:00", false, typeof(DateTime))]
public void ConvertTo_StrictModeEnabled_Throws(string input, bool shouldThrow, Type targetType)
{
var options = new ObjectConverterOptions(StrictMode: true);
if(shouldThrow)
Assert.Throws<TypeConversionException>(() => input.ConvertTo(targetType, options));
}
}
@@ -0,0 +1 @@
global using Xunit;
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,230 @@
using Elsa.Common;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Models;
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<WorkflowDefinition> _workflowDefinitionsInStore = new();
public DefaultWorkflowDefinitionStorePopulatorTests()
{
_storeMock = Substitute.For<IWorkflowDefinitionStore>();
_storeMock.FindManyAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>())
.Returns(_workflowDefinitionsInStore);
_populator = new DefaultWorkflowDefinitionStorePopulator(() => new List<IWorkflowsProvider>(),
Substitute.For<ITriggerIndexer>(),
_storeMock,
Substitute.For<IActivitySerializer>(),
Substitute.For<IPayloadSerializer>(),
Substitute.For<ISystemClock>(),
Substitute.For<IIdentityGraphService>(),
Substitute.For<ILogger<DefaultWorkflowDefinitionStorePopulator>>());
}
[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<InputDefinition>
{
new()
{
Name = "InputA"
}
}
});
var workflow = new MaterializedWorkflow(new Workflow
{
Identity = new WorkflowIdentity("a", 1, "1"),
Inputs = new List<InputDefinition>
{
new()
{
Name = "A"
}
}
}, "Test", "Test");
await _populator.AddAsync(workflow);
await _storeMock.DidNotReceive().SaveManyAsync(Arg.Any<IEnumerable<WorkflowDefinition>>(), Arg.Any<CancellationToken>());
}
[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<IEnumerable<WorkflowDefinition>>(), Arg.Any<CancellationToken>());
}
[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<IEnumerable<WorkflowDefinition>>(l =>
l.Any(wd => wd.Id == "2")), Arg.Any<CancellationToken>());
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<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>())
.Returns(new List<WorkflowDefinition>
{
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<IEnumerable<WorkflowDefinition>>(l =>
l.Any(wd => wd.Id == "2")), Arg.Any<CancellationToken>());
await _storeMock.Received().SaveManyAsync(Arg.Is<IEnumerable<WorkflowDefinition>>(l =>
l.Any(wd => wd.Id == "1" && wd.IsPublished == false)), Arg.Any<CancellationToken>());
}
private async Task CheckIfWorkflowWasSaved(WorkflowIdentity identity, Func<WorkflowDefinition, bool> predicate)
{
await _storeMock.Received(1).SaveManyAsync(Arg.Is<IEnumerable<WorkflowDefinition>>(l =>
l.Where(wd => wd.Id == identity.Id
&& wd.DefinitionId == identity.DefinitionId
&& wd.Version == identity.Version).Any(predicate)), Arg.Any<CancellationToken>());
}
private async Task CheckIfWorkflowWasSaved(Func<WorkflowDefinition, bool> predicate)
{
await _storeMock.Received(1).SaveManyAsync(Arg.Is<IEnumerable<WorkflowDefinition>>(l =>
l.Any(predicate)), Arg.Any<CancellationToken>());
}
private async Task CheckAmountOfWorkflowsSaved(int count)
{
await _storeMock.Received(count)
.SaveManyAsync(Arg.Any<IEnumerable<WorkflowDefinition>>(), Arg.Any<CancellationToken>());
}
}
@@ -0,0 +1 @@
global using Xunit;