diff --git a/src/Take.Blip.Builder.Benchmark/Actions/ExecuteScriptBenchmarkTests.cs b/src/Take.Blip.Builder.Benchmark/Actions/ExecuteScriptBenchmarkTests.cs
new file mode 100644
index 00000000..91896f7d
--- /dev/null
+++ b/src/Take.Blip.Builder.Benchmark/Actions/ExecuteScriptBenchmarkTests.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Order;
+using NSubstitute;
+using Serilog;
+using Take.Blip.Builder.Actions.ExecuteScript;
+using Take.Blip.Builder.Actions.ExecuteScriptV2;
+using Take.Blip.Builder.Benchmark.Context;
+using Take.Blip.Builder.Hosting;
+using Take.Blip.Builder.Utils;
+
+namespace Take.Blip.Builder.Benchmark.Actions
+{
+ ///
+ [MemoryDiagnoser]
+ [Orderer(SummaryOrderPolicy.FastestToSlowest)]
+ public class ExecuteScriptBenchmarkTests : ActionTestsBase
+ {
+ private ExecuteScriptV2Action _v2Action;
+ private ExecuteScriptAction _v1Action;
+
+ ///
+ /// Setup the benchmark tests.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ var configuration = new TestConfiguration();
+ var conventions = new ConventionsConfiguration();
+
+ configuration.ExecuteScriptV2Timeout = TimeSpan.FromMilliseconds(300);
+ configuration.ExecuteScriptV2MaxRuntimeHeapSize =
+ conventions.ExecuteScriptV2MaxRuntimeHeapSize;
+ configuration.ExecuteScriptV2MaxRuntimeStackUsage =
+ conventions.ExecuteScriptV2MaxRuntimeStackUsage;
+
+ configuration.ExecuteScriptTimeout = TimeSpan.FromMilliseconds(300);
+ configuration.ExecuteScriptLimitMemory = conventions.ExecuteScriptLimitMemory;
+ configuration.ExecuteScriptLimitRecursion = 100000;
+ configuration.ExecuteScriptMaxStatements = 0;
+
+ _v2Action = new ExecuteScriptV2Action(configuration, Substitute.For(),
+ Substitute.For());
+
+ _v1Action = new ExecuteScriptAction(configuration, Substitute.For());
+ }
+
+ ///
+ /// Execute a loop script using the V1 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV1LoopScript()
+ {
+ await _v1Action.ExecuteAsync(Context, Settings._v1LoopSettings, CancellationToken);
+ }
+
+ ///
+ /// Execute a loop script using the V2 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV2LoopScript()
+ {
+ await _v2Action.ExecuteAsync(Context, Settings._v2LoopSettings, CancellationToken);
+ }
+
+ ///
+ /// Execute a math script using the V1 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV1MathScript()
+ {
+ await _v1Action.ExecuteAsync(Context, Settings._v1MathSettings, CancellationToken);
+ }
+
+ ///
+ /// Execute a math script using the V2 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV2MathScript()
+ {
+ await _v2Action.ExecuteAsync(Context, Settings._v2MathSettings, CancellationToken);
+ }
+
+ ///
+ /// Execute a json script using the V1 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV1JsonScript()
+ {
+ await _v1Action.ExecuteAsync(Context, Settings._v1JsonSettings, CancellationToken);
+ }
+
+ ///
+ /// Execute a json script using the V2 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV2JsonScript()
+ {
+ await _v2Action.ExecuteAsync(Context, Settings._v2JsonSettings, CancellationToken);
+ }
+
+ ///
+ /// Execute a simple script using the V1 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV1SimpleScript()
+ {
+ await _v1Action.ExecuteAsync(Context, Settings._v1SimpleSettings, CancellationToken);
+ }
+
+ ///
+ /// Execute a simple script using the V2 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV2SimpleScript()
+ {
+ await _v2Action.ExecuteAsync(Context, Settings._v2SimpleSettings, CancellationToken);
+ }
+
+ ///
+ /// Execute a recursion loop script using the V1 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV1RecursionLoopScript()
+ {
+ await _v1Action.ExecuteAsync(Context, Settings._v1RecursionLoopSettings,
+ CancellationToken);
+ }
+
+ ///
+ /// Execute a recursion loop script using the V2 action.
+ ///
+ [Benchmark]
+ public async Task ExecuteScriptV2RecursionLoopScript()
+ {
+ await _v2Action.ExecuteAsync(Context, Settings._v2RecursionLoopSettings,
+ CancellationToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Take.Blip.Builder.Benchmark/Actions/Settings.cs b/src/Take.Blip.Builder.Benchmark/Actions/Settings.cs
new file mode 100644
index 00000000..7c710355
--- /dev/null
+++ b/src/Take.Blip.Builder.Benchmark/Actions/Settings.cs
@@ -0,0 +1,157 @@
+using Newtonsoft.Json.Linq;
+using Take.Blip.Builder.Actions.ExecuteScript;
+using Take.Blip.Builder.Actions.ExecuteScriptV2;
+
+namespace Take.Blip.Builder.Benchmark.Actions;
+
+internal static class Settings
+{
+ private const string SIMPLE_SCRIPT = """
+ function run() {
+ return 'a';
+ }
+ """;
+
+ private const string LOOP_SCRIPT = """
+ function run() {
+ let a = 0;
+
+ while (a < 1000) {
+ a++;
+ }
+
+ return a;
+ }
+ """;
+
+ private const string RECURSION_LOOP_SCRIPT = """
+ function recursiveLoop(a) {
+ if (a < 1000) {
+ return recursiveLoop(a + 1);
+ }
+ return a;
+ }
+
+ function run() {
+ return recursiveLoop(0);
+ }
+ """;
+
+ private const string JSON_SCRIPT = """
+ function run() {
+ const json = { 'a': 1, 'b': 'c', 'd' : ['1', '2'], 'e': { 'f': 1 } };
+
+ let stringJson = JSON.stringify(json);
+
+ let parsedJson = JSON.parse(stringJson);
+
+ return parsedJson.a + parsedJson.e.f;
+ }
+ """;
+
+ private const string MATH_SCRIPT = """
+ function add(a, b) {
+ return a + b;
+ }
+
+ function subtract(a, b) {
+ return a - b;
+ }
+
+ function multiply(a, b) {
+ return a * b;
+ }
+
+ function divide(a, b) {
+ if (b == 0) {
+ throw 'Division by zero';
+ }
+ return a / b;
+ }
+
+ function calculate(a, b) {
+ let sum = add(a, b);
+ let difference = subtract(a, b);
+ let product = multiply(a, b);
+ let quotient = divide(a, b);
+
+ return {
+ 'sum': sum,
+ 'difference': difference,
+ 'product': product,
+ 'quotient': quotient
+ };
+ }
+
+ function run() {
+ let a = 10;
+ let b = 2;
+
+ let result = calculate(a, b);
+
+ let finalResult = result.sum * result.difference / result.quotient;
+
+ return finalResult;
+ }
+ """;
+
+ internal static readonly JObject _v1LoopSettings = JObject.FromObject(
+ new ExecuteScriptSettings
+ {
+ OutputVariable = "result", Function = "run", Source = LOOP_SCRIPT
+ });
+
+ internal static readonly JObject _v2LoopSettings = JObject.FromObject(
+ new ExecuteScriptV2Settings
+ {
+ OutputVariable = "result", Function = "run", Source = LOOP_SCRIPT
+ });
+
+ internal static readonly JObject _v1MathSettings = JObject.FromObject(
+ new ExecuteScriptSettings
+ {
+ OutputVariable = "result", Function = "run", Source = MATH_SCRIPT
+ });
+
+ internal static readonly JObject _v2MathSettings = JObject.FromObject(
+ new ExecuteScriptV2Settings
+ {
+ OutputVariable = "result", Function = "run", Source = MATH_SCRIPT
+ });
+
+ internal static readonly JObject _v1JsonSettings = JObject.FromObject(
+ new ExecuteScriptSettings
+ {
+ OutputVariable = "result", Function = "run", Source = JSON_SCRIPT
+ });
+
+ internal static readonly JObject _v2JsonSettings = JObject.FromObject(
+ new ExecuteScriptV2Settings
+ {
+ OutputVariable = "result", Function = "run", Source = JSON_SCRIPT
+ });
+
+ internal static readonly JObject _v1SimpleSettings = JObject.FromObject(
+ new ExecuteScriptSettings
+ {
+ OutputVariable = "result", Function = "run", Source = SIMPLE_SCRIPT
+ });
+
+ internal static readonly JObject _v2SimpleSettings = JObject.FromObject(
+ new ExecuteScriptV2Settings
+ {
+ OutputVariable = "result", Function = "run", Source = SIMPLE_SCRIPT
+ });
+
+ internal static readonly JObject _v1RecursionLoopSettings = JObject.FromObject(
+ new ExecuteScriptSettings
+ {
+ OutputVariable = "result", Function = "run", Source = RECURSION_LOOP_SCRIPT
+ });
+
+ internal static readonly JObject _v2RecursionLoopSettings = JObject.FromObject(
+ new ExecuteScriptV2Settings
+ {
+ OutputVariable = "result", Function = "run", Source = RECURSION_LOOP_SCRIPT
+ });
+}
\ No newline at end of file
diff --git a/src/Take.Blip.Builder.Benchmark/Actions/TestConfiguration.cs b/src/Take.Blip.Builder.Benchmark/Actions/TestConfiguration.cs
new file mode 100644
index 00000000..7e64b350
--- /dev/null
+++ b/src/Take.Blip.Builder.Benchmark/Actions/TestConfiguration.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Take.Blip.Builder.Hosting;
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+namespace Take.Blip.Builder.Benchmark.Actions
+{
+ ///
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
+ public class TestConfiguration : IConfiguration
+ {
+ public TimeSpan InputProcessingTimeout { get; set; }
+ public int RedisDatabase { get; set; }
+ public string RedisKeyPrefix { get; set; }
+ public string InternalUris { get; set; }
+ public int MaxTransitionsByInput { get; set; }
+ public int TraceQueueBoundedCapacity { get; set; }
+ public int TraceQueueMaxDegreeOfParallelism { get; set; }
+ public TimeSpan TraceTimeout { get; set; }
+ public TimeSpan DefaultActionExecutionTimeout { get; set; }
+ public int ExecuteScriptLimitRecursion { get; set; }
+ public int ExecuteScriptMaxStatements { get; set; }
+ public long ExecuteScriptLimitMemory { get; set; }
+ public long ExecuteScriptLimitMemoryWarning { get; set; }
+ public TimeSpan ExecuteScriptTimeout { get; set; }
+ public TimeSpan ExecuteScriptV2Timeout { get; set; }
+ public int MaximumInputExpirationLoop { get; set; }
+ public long ExecuteScriptV2MaxRuntimeHeapSize { get; set; }
+ public long ExecuteScriptV2MaxRuntimeStackUsage { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Take.Blip.Builder.Benchmark/Context/ActionTestsBase.cs b/src/Take.Blip.Builder.Benchmark/Context/ActionTestsBase.cs
new file mode 100644
index 00000000..81a00a74
--- /dev/null
+++ b/src/Take.Blip.Builder.Benchmark/Context/ActionTestsBase.cs
@@ -0,0 +1,43 @@
+using Lime.Messaging.Contents;
+using Lime.Protocol;
+using Lime.Protocol.Serialization;
+using Lime.Protocol.Serialization.Newtonsoft;
+using NSubstitute;
+using Take.Blip.Builder.Models;
+
+namespace Take.Blip.Builder.Benchmark.Context;
+
+///
+public class ActionTestsBase : ContextTestsBase
+{
+ ///
+ protected ActionTestsBase()
+ {
+ Context.Flow.Returns(Flow);
+ From = UserIdentity.ToNode();
+ To = OwnerIdentity.ToNode();
+ Message = new Message()
+ {
+ From = From, To = To, Content = new PlainText { Text = "Hello BLiP" }
+ };
+ Input = new LazyInput(
+ Message,
+ UserIdentity,
+ new BuilderConfiguration(),
+ new DocumentSerializer(new DocumentTypeResolver()),
+ new EnvelopeSerializer(new DocumentTypeResolver()),
+ null,
+ CancellationToken);
+ Context.Input.Returns(Input);
+ Context.OwnerIdentity.Returns(OwnerIdentity);
+ Context.UserIdentity.Returns(UserIdentity);
+ }
+
+ private Node From { get; }
+
+ private Node To { get; }
+
+ private Message Message { get; }
+
+ private LazyInput Input { get; }
+}
\ No newline at end of file
diff --git a/src/Take.Blip.Builder.Benchmark/Context/CancellationTokenTestsBase.cs b/src/Take.Blip.Builder.Benchmark/Context/CancellationTokenTestsBase.cs
new file mode 100644
index 00000000..adbbab96
--- /dev/null
+++ b/src/Take.Blip.Builder.Benchmark/Context/CancellationTokenTestsBase.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+
+namespace Take.Blip.Builder.Benchmark.Context;
+
+///
+public class CancellationTokenTestsBase : IDisposable
+{
+ ///
+ /// The cancellation token.
+ ///
+ protected CancellationToken CancellationToken => CancellationTokenSource.Token;
+
+ private CancellationTokenSource CancellationTokenSource { get; set; } =
+ new(TimeSpan.FromSeconds(30));
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Dispose the resources.
+ ///
+ ///
+ [SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")]
+ protected virtual void Dispose(bool disposing)
+ {
+ // Cleanup
+
+ if (disposing)
+ {
+ CancellationTokenSource?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Take.Blip.Builder.Benchmark/Context/ContextTestsBase.cs b/src/Take.Blip.Builder.Benchmark/Context/ContextTestsBase.cs
new file mode 100644
index 00000000..69de5467
--- /dev/null
+++ b/src/Take.Blip.Builder.Benchmark/Context/ContextTestsBase.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using Lime.Protocol;
+using NSubstitute;
+using Take.Blip.Builder.Models;
+
+namespace Take.Blip.Builder.Benchmark.Context;
+
+///
+public class ContextTestsBase : CancellationTokenTestsBase
+{
+ ///
+ protected ContextTestsBase()
+ {
+ Context = Substitute.For();
+ Flow = new Flow { Configuration = new Dictionary() };
+ UserIdentity = new Identity(Guid.NewGuid().ToString(), "msging.net");
+ OwnerIdentity = new Identity("application", "msging.net");
+ Context.Flow.Returns(Flow);
+ Context.UserIdentity.Returns(UserIdentity);
+ Context.OwnerIdentity.Returns(OwnerIdentity);
+ }
+
+ ///
+ /// The context instance.
+ ///
+ protected IContext Context { get; set; }
+
+ ///
+ /// The flow instance.
+ ///
+ protected Flow Flow { get; }
+
+ ///
+ /// The user identity.
+ ///
+ protected Identity UserIdentity { get; }
+
+ ///
+ /// The owner identity.
+ ///
+ protected Identity OwnerIdentity { get; }
+}
\ No newline at end of file
diff --git a/src/Take.Blip.Builder.Benchmark/Program.cs b/src/Take.Blip.Builder.Benchmark/Program.cs
new file mode 100644
index 00000000..778523b9
--- /dev/null
+++ b/src/Take.Blip.Builder.Benchmark/Program.cs
@@ -0,0 +1,4 @@
+using BenchmarkDotNet.Running;
+using Take.Blip.Builder.Benchmark.Actions;
+
+BenchmarkRunner.Run();
\ No newline at end of file
diff --git a/src/Take.Blip.Builder.Benchmark/Take.Blip.Builder.Benchmark.csproj b/src/Take.Blip.Builder.Benchmark/Take.Blip.Builder.Benchmark.csproj
new file mode 100644
index 00000000..1e958f20
--- /dev/null
+++ b/src/Take.Blip.Builder.Benchmark/Take.Blip.Builder.Benchmark.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ true
+ True
+ latest
+ Exe
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Take.Blip.Builder.UnitTests/Actions/ExecuteScript2ActionTests.cs b/src/Take.Blip.Builder.UnitTests/Actions/ExecuteScript2ActionTests.cs
new file mode 100644
index 00000000..6e922a24
--- /dev/null
+++ b/src/Take.Blip.Builder.UnitTests/Actions/ExecuteScript2ActionTests.cs
@@ -0,0 +1,1002 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.ClearScript;
+using Newtonsoft.Json.Linq;
+using NSubstitute;
+using Serilog;
+using Shouldly;
+using Take.Blip.Builder.Actions.ExecuteScriptV2;
+using Take.Blip.Builder.Hosting;
+using Take.Blip.Builder.Utils;
+using Xunit;
+
+namespace Take.Blip.Builder.UnitTests.Actions
+{
+ public class ExecuteScript2ActionTests : ActionTestsBase
+ {
+ private static ExecuteScriptV2Action GetTarget(IHttpClient client = null)
+ {
+ var configuration = new TestConfiguration();
+ var conventions = new ConventionsConfiguration();
+
+ configuration.ExecuteScriptV2Timeout = TimeSpan.FromMilliseconds(300);
+ configuration.ExecuteScriptV2MaxRuntimeHeapSize =
+ conventions.ExecuteScriptV2MaxRuntimeHeapSize;
+ configuration.ExecuteScriptV2MaxRuntimeStackUsage =
+ conventions.ExecuteScriptV2MaxRuntimeStackUsage;
+
+ return new ExecuteScriptV2Action(configuration, client ?? Substitute.For(),
+ Substitute.For());
+ }
+
+ [Fact]
+ public async Task ExecuteWithSingleStatementScriptShouldSucceed()
+ {
+ // Arrange
+ const string variableName = "variable1";
+ const string variableValue = "my variable 1 value";
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = $"function run() {{ return '{variableValue}'; }}",
+ OutputVariable = variableName
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Any(),
+ CancellationToken, Arg.Any());
+ await Context.Received(1).SetVariableAsync(variableName, variableValue,
+ CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptParseIntWithManyChars()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+function run() {
+ let numberTest = new Array(100000).join('Z');
+
+ let convert = parseInt(numberTest);
+
+ return convert;
+}
+",
+ OutputVariable = "test"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("test", "NaN", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptWithLiteralRegularExpression()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+const matchEmailRegex = (input) => {
+ const pattern = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/gmi;
+ return input.match(pattern, 'gmi');
+}
+
+function run() {
+ return matchEmailRegex('test@blip.ai');
+}
+",
+ OutputVariable = "test"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("test", "[\"test@blip.ai\"]", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithCustomTimeZoneDateStringAndTimeStringShouldWork()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ // Fixed date to test timezone
+ Source =
+ "function run() { return time.parseDate('2021-01-01T00:00:10').toDateString() + ' ' + time.parseDate('2021-01-01T00:00:10').toTimeString(); }",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ Context.Flow.Configuration["builder:#localTimeZone"] = "Asia/Shanghai";
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ // Jint doesn't support toLocaleString, so it will return the default date format
+ await Context.Received(1).SetVariableAsync("test", "Fri Jan 01 2021 11:00:10 GMT+08:00",
+ CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithCustomTimeZoneDateStringAndTimeStringWithAmericaShouldWork()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ // Fixed date to test timezone
+ Source =
+ "function run() { return time.parseDate('2021-01-01T00:00:10').toDateString() + ' ' + time.parseDate('2021-01-01T00:00:10').toTimeString(); }",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ Context.Flow.Configuration["builder:#localTimeZone"] = "America/Sao_Paulo";
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ // Jint doesn't support toLocaleString, so it will return the default date format
+ await Context.Received(1).SetVariableAsync("test",
+ "Fri Jan 01 2021 00:00:10 GMT-03:00", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithCustomTimeZoneStringMethodsShouldBeTheSame()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ // Fixed date to test timezone
+ Source = @"
+function run() {
+ var parsedDate = time.parseDate('2021-01-01T00:00:10');
+
+ return (parsedDate.toDateString() + ' ' + parsedDate.toTimeString()) == parsedDate.toString();
+}",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ Context.Flow.Configuration["builder:#localTimeZone"] = "Asia/Shanghai";
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ // Jint doesn't support toLocaleString, so it will return the default date format
+ await Context.Received(1).SetVariableAsync("test", "true", CancellationToken);
+ }
+
+
+ [Fact]
+ public async Task ExecuteThrowExceptionTest()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = $"function run() {{ throw new Error('Test error'); }}",
+ OutputVariable = "variable1",
+ CaptureExceptions = true,
+ ExceptionVariable = "exception"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("exception", "Error: Test error",
+ CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithArgumentsShouldSucceed()
+ {
+ // Arrange
+ const string number1 = "100";
+ const string number2 = "250";
+ Context.GetVariableAsync(nameof(number1), CancellationToken).Returns(number1);
+ Context.GetVariableAsync(nameof(number2), CancellationToken).Returns(number2);
+
+ var settings = new ExecuteScriptV2Settings
+ {
+ InputVariables = new[] { nameof(number1), nameof(number2) },
+ Source = @"
+function run(number1, number2) {
+ return parseInt(number1) + parseInt(number2);
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Any(),
+ CancellationToken, Arg.Any());
+ await Context.Received(1).SetVariableAsync("result", "350", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteSetContextVariableShouldSucceed()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+async function run() {
+ await context.setVariableAsync('test', 100);
+
+ return true;
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("test", "100", Arg.Any());
+ await Context.Received(1)
+ .SetVariableAsync("result", "true", Arg.Any());
+ }
+
+ [Fact]
+ public async Task ExecuteMultipleAsyncResults()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+async function testNum() {
+ return 1;
+}
+
+async function testStr() {
+ return 'bla';
+}
+
+async function testRecursiveAsync() {
+ return await testStr();
+}
+
+async function run() {
+ return {
+ 'num': testNum(),
+ 'str': testStr(),
+ 'recursive': testRecursiveAsync()
+ };
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("result",
+ "{\"num\":1,\"str\":\"bla\",\"recursive\":\"bla\"}", Arg.Any());
+ }
+
+ [Fact]
+ public async Task ExecuteWithMissingArgumentsShouldSucceed()
+ {
+ // Arrange
+ const string number1 = "100";
+ const string number2 = "250";
+ Context.GetVariableAsync(nameof(number1), CancellationToken).Returns(number1);
+ Context.GetVariableAsync(nameof(number2), CancellationToken).Returns(number2);
+
+ var settings = new ExecuteScriptV2Settings
+ {
+ InputVariables = new[] { nameof(number1), nameof(number2) },
+ Source = @"
+function run(number1, number2, number3) {
+ return parseInt(number1) + parseInt(number2) + (number3 || 150);
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Any(),
+ CancellationToken, Arg.Any());
+ await Context.Received(1).SetVariableAsync("result", "500", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteUsingLetAndConstVariablesShouldHaveScopeAndSucceed()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ InputVariables = Array.Empty(),
+ Source = @"
+function scopedFunc() {
+ let x = 1;
+ const y = 'my value';
+ return { x: x, y: y };
+}
+
+function run() {
+ var scopedReturn = scopedFunc();
+ return typeof x === 'undefined' && typeof y === 'undefined' && scopedReturn.x === 1 && scopedReturn.y === 'my value';
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("result", "true", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithCustomFunctionNameAndArgumentsShouldSucceed()
+ {
+ // Arrange
+ const string number1 = "100";
+ const string number2 = "250";
+ Context.GetVariableAsync(nameof(number1), CancellationToken).Returns(number1);
+ Context.GetVariableAsync(nameof(number2), CancellationToken).Returns(number2);
+
+ var settings = new ExecuteScriptV2Settings
+ {
+ Function = "executeFunc",
+ InputVariables = new[] { nameof(number1), nameof(number2) },
+ Source = @"
+function executeFunc(number1, number2) {
+ return parseInt(number1) + parseInt(number2);
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Any(),
+ CancellationToken, Arg.Any());
+ await Context.Received(1).SetVariableAsync("result", "350", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithJsonReturnValueShouldSucceed()
+ {
+ // Arrange
+ var result =
+ "{\"id\":1,\"valid\":true,\"options\":[1,2,3],\"names\":[\"a\",\"b\",\"c\"],\"others\":[{\"a\":\"value1\"},{\"b\":\"value2\"}],\"content\":{\"uri\":\"https://server.com/image.jpeg\",\"type\":\"image/jpeg\"}}";
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+function run() {
+ return {
+ id: 1,
+ valid: true,
+ options: [ 1, 2, 3 ],
+ names: [ 'a', 'b', 'c' ],
+ others: [{ a: 'value1' }, { b: 'value2' }],
+ content: {
+ uri: 'https://server.com/image.jpeg',
+ type: 'image/jpeg'
+ }
+ };
+}
+",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Any(),
+ CancellationToken, Arg.Any());
+ await Context.Received(1).SetVariableAsync("result", result, CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithArrayReturnValueShouldSucceed()
+ {
+ // Arrange
+ const string result = "[1,2,3]";
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+function run() {
+ return [1, 2, 3];
+}
+",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Any(),
+ CancellationToken, Arg.Any());
+ await Context.Received(1).SetVariableAsync("result", result, CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithWhileTrueShouldFail()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+function run() {
+ var value = 0;
+ while (true) {
+ value++;
+ }
+ return value;
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ try
+ {
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+ throw new Exception("The script was executed");
+ }
+ catch (TimeoutException ex)
+ {
+ ex.Message.ShouldBe("Script execution timed out");
+ }
+ }
+
+
+ [Fact]
+ public async Task ExecuteWithDefaultTimeZoneShouldWork()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ // Test date parsing and also converting to specific format and timezone
+ Source =
+ "function run() { return time.parseDate('2021-01-01T00:00:00Z', {format:'yyyy-MM-ddTHH:mm:ssZ'}).toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' }); }",
+ OutputVariable = "test"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1)
+ .SetVariableAsync("test", "31/12/2020, 21:00:00", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteParseDateOverloads()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ // Test date parsing and also converting to specific format and timezone
+ Source =
+ @"
+function run() {
+ return {
+ 'parseDate': time.parseDate('2021-01-01T19:01:01.0000001+08:00'),
+ 'parseDateWithFormat': time.parseDate('01/02/2021', {format:'MM/dd/yyyy'}),
+ 'parseDateWithFormatAndCulture': time.parseDate('01/01/2021', {format: 'MM/dd/yyyy', culture: 'pt-BR'}),
+ }
+}",
+ OutputVariable = "test"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1)
+ .SetVariableAsync("test",
+ "{\"parseDate\":\"2021-01-01T08:01:01.0000000-03:00\",\"parseDateWithFormat\":\"2021-01-02T00:00:00.0000000-03:00\",\"parseDateWithFormatAndCulture\":\"2021-01-01T00:00:00.0000000-03:00\"}",
+ CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithCustomTimeZoneShouldWork()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ // Test date parsing from GMT, with bot on Asia/Shanghai (+8 from GMT) and then converting to SP (-3 from GMT)
+ Source =
+ "function run() { return time.parseDate('2021-01-01T00:00:00Z', {format:'yyyy-MM-ddTHH:mm:ssZ'}).toLocaleString('en-US', { timeZone: 'America/Sao_Paulo' }); }",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ Context.Flow.Configuration["builder:#localTimeZone"] = "Asia/Shanghai";
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("test", "12/31/2020, 9:00:00 PM",
+ CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithArrowFunctionOnRun()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source =
+ @"
+anArrowFunction = () => {
+ return 'foo';
+}
+
+async function run() {
+ return anArrowFunction();
+}
+",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("test", "foo", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithArrowFunctionEntrypointShouldWork()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source =
+ @"
+run = async () => {
+ return 'foo';
+}
+",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("test", "foo", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteDateToStringWithCustomTimeZoneShouldWork()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ // Test date parsing and also converting to specific format and timezone
+ Source =
+ "function run() { return time.dateToString(time.parseDate('2021-01-01T00:00:00Z', {'format':'yyyy-MM-ddTHH:mm:ssZ'})); }",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ Context.Flow.Configuration["builder:#localTimeZone"] = "Asia/Shanghai";
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("test", "2021-01-01T08:00:00.0000000+08:00",
+ CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteParseDateWithDefaultFormat()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ // Test date parsing and also converting to specific format and timezone
+ Source =
+ @"
+function run() {
+ var parsed = time.parseDate('2021-01-01T19:00:00.0000000');
+
+ var stringDate = time.dateToString(parsed);
+
+ return {
+ 'parsed': parsed,
+ 'stringDate': stringDate
+ }
+}",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ Context.Flow.Configuration["builder:#localTimeZone"] = "America/Sao_Paulo";
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("test",
+ "{\"parsed\":\"2021-01-01T19:00:00.0000000-03:00\",\"stringDate\":\"2021-01-01T19:00:00.0000000-03:00\"}",
+ CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithInfiniteSleepShouldFail()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+function run() {
+ time.sleep(1000000000);
+
+ return value;
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ try
+ {
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+ throw new Exception("The script was executed");
+ }
+ catch (TimeoutException ex)
+ {
+ ex.Message.ShouldBe("Script execution timed out");
+ }
+ catch (ScriptEngineException ex)
+ {
+ ex.Message.ShouldBe("Error: Script execution timed out");
+ }
+ }
+
+ [Fact]
+ public async Task ExecuteScripWithXmlHttpRequestShouldFail()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+function run() {
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == XMLHttpRequest.DONE) {
+ alert(xhr.responseText);
+ }
+ }
+ xhr.open('GET', 'https://example.com', true);
+ xhr.send(null);
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ try
+ {
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+ throw new Exception("The script was executed");
+ }
+ catch (ScriptEngineException ex)
+ {
+ ex.Message.ShouldContain("XMLHttpRequest is not defined");
+ }
+ }
+
+ [Fact]
+ [SuppressMessage("Performance", "CA1861:Avoid constant arrays as arguments")]
+ public async Task ExecuteScriptWithFetchRequestShouldSucceed()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+async function run() {
+ var response = await request.fetchAsync('https://mock.com', {
+ 'method': 'POST',
+ 'body': 'r8eht438thj9848',
+ 'headers': {
+ 'Content-Type': 'application/text',
+ 'test': 'test2',
+ 'test2': ['bla', 'bla2']
+ }
+ });
+
+ return response;
+}",
+ OutputVariable = "result"
+ };
+
+ using var response = new HttpResponseMessage();
+ response.StatusCode = HttpStatusCode.OK;
+ response.Content = new StringContent("{\"result\": \"bla\"}");
+ response.Headers.Add("test", "test2");
+ response.Headers.Add("test2", new[] { "bla", "bla2" });
+
+ var httpClient = Substitute.For();
+
+ HttpRequestMessage resultMessage = null;
+ httpClient.SendAsync(Arg.Do(message =>
+ {
+ resultMessage = new HttpRequestMessage
+ {
+ Method = message.Method,
+ RequestUri = message.RequestUri,
+ Content = new StringContent(message.Content!.ReadAsStringAsync()
+ .GetAwaiter()
+ .GetResult(), Encoding.UTF8,
+ message.Content.Headers.ContentType?.MediaType!)
+ };
+
+ for (var i = 0; i < message.Headers.Count(); i++)
+ {
+ var header = message.Headers.ElementAt(i);
+ resultMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }), Arg.Any())
+ .Returns(response);
+
+ var target = GetTarget(httpClient);
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("result",
+ "{\"status\":200,\"success\":true,\"body\":\"{\\\"result\\\": \\\"bla\\\"}\",\"headers\":{\"test\":[\"test2\"],\"test2\":[\"bla\",\"bla2\"]}}",
+ CancellationToken);
+
+ resultMessage.Method.ShouldBe(HttpMethod.Post);
+ resultMessage.RequestUri.ShouldBe(new Uri("https://mock.com"));
+
+ var requestBody = await resultMessage.Content!.ReadAsStringAsync();
+
+ requestBody.ShouldBe("r8eht438thj9848");
+
+ resultMessage.Headers.GetValues("test").First().ShouldBe("test2");
+ resultMessage.Headers.GetValues("test2").ShouldBe(new[] { "bla", "bla2" });
+
+ resultMessage.Content.Headers.ContentType!.MediaType.ShouldBe("application/text");
+ }
+
+ [Fact]
+ [SuppressMessage("Performance", "CA1861:Avoid constant arrays as arguments")]
+ public async Task ExecuteScriptWithRequestParseJsonResponse()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+async function run() {
+ var response = await request.fetchAsync('https://mock.com', {
+ 'method': 'POST',
+ 'body': 'r8eht438thj9848',
+ 'headers': {
+ 'Content-Type': 'application/text',
+ 'test': 'test2',
+ 'test2': ['bla', 'bla2']
+ }
+ });
+
+ return await response.jsonAsync();
+}",
+ OutputVariable = "result"
+ };
+
+ using var response = new HttpResponseMessage();
+ response.StatusCode = HttpStatusCode.OK;
+ response.Content =
+ new StringContent("{\"result\": \"bla\"}", Encoding.UTF8, "application/json");
+ response.Headers.Add("test", "test2");
+ response.Headers.Add("test2", new[] { "bla", "bla2" });
+
+ var httpClient = Substitute.For();
+
+ httpClient.SendAsync(Arg.Any(), Arg.Any())
+ .Returns(response);
+
+ var target = GetTarget(httpClient);
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("result",
+ "{\"result\":\"bla\"}",
+ CancellationToken);
+ }
+
+ [Fact]
+ [SuppressMessage("Performance", "CA1861:Avoid constant arrays as arguments")]
+ public async Task ExecuteScriptWithFetchRequestWithoutOptionsShouldSucceed()
+ {
+ // Arrange
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+async function run() {
+ var response = await request.fetchAsync('https://mock.com');
+
+ return response;
+}",
+ OutputVariable = "result"
+ };
+
+ using var response = new HttpResponseMessage();
+ response.StatusCode = HttpStatusCode.OK;
+ response.Content = new StringContent("{\"result\": \"bla\"}");
+ response.Headers.Add("test", "test2");
+ response.Headers.Add("test2", new[] { "bla", "bla2" });
+
+ var httpClient = Substitute.For();
+
+ HttpRequestMessage resultMessage = null;
+ httpClient.SendAsync(Arg.Do(message =>
+ {
+ resultMessage = new HttpRequestMessage
+ {
+ Method = message.Method,
+ RequestUri = message.RequestUri,
+ Content = message.Content,
+ };
+
+ for (var i = 0; i < message.Headers.Count(); i++)
+ {
+ var header = message.Headers.ElementAt(i);
+ resultMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
+ }
+ }), Arg.Any())
+ .Returns(response);
+
+ var target = GetTarget(httpClient);
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync("result",
+ "{\"status\":200,\"success\":true,\"body\":\"{\\\"result\\\": \\\"bla\\\"}\",\"headers\":{\"test\":[\"test2\"],\"test2\":[\"bla\",\"bla2\"]}}",
+ CancellationToken);
+
+ resultMessage.Method.ShouldBe(HttpMethod.Get);
+ resultMessage.RequestUri.ShouldBe(new Uri("https://mock.com"));
+ resultMessage.Content.ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task ExecuteScriptParseIntWithExceededLengthShouldSucceed()
+ {
+ // Arrange
+ var result = "NaN";
+ var settings = new ExecuteScriptV2Settings
+ {
+ Source = @"
+function run() {
+ let numberTest = new Array(100000).join('Z');
+ let convert = parseInt(numberTest);
+ return convert;
+}",
+ OutputVariable = "result"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Any(),
+ CancellationToken, Arg.Any());
+ await Context.Received(1).SetVariableAsync("result", result, CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteScriptJsonParseWithSpecialCharacterShouldSucceed()
+ {
+ // Arrange
+ var invalidCharacter = "?";
+ Context.GetVariableAsync(nameof(invalidCharacter), CancellationToken)
+ .Returns(invalidCharacter);
+ var result = "{\"value\":\"\"}";
+
+ var settings = new ExecuteScriptV2Settings
+ {
+ InputVariables = new[] { nameof(invalidCharacter) },
+ Source = @"
+function run (input) {
+ try {
+ return JSON.parse(input);
+ } catch (e) {
+ return {
+ value: ''
+ };
+ }
+}",
+ OutputVariable = "result"
+ };
+
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ await Context.Received(1).SetVariableAsync(Arg.Any(), Arg.Any(),
+ CancellationToken, Arg.Any());
+ await Context.Received(1).SetVariableAsync("result", result, CancellationToken);
+ }
+
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
+ private class TestConfiguration : IConfiguration
+ {
+ public TimeSpan InputProcessingTimeout { get; set; }
+ public int RedisDatabase { get; set; }
+ public string RedisKeyPrefix { get; set; }
+ public string InternalUris { get; set; }
+ public int MaxTransitionsByInput { get; set; }
+ public int TraceQueueBoundedCapacity { get; set; }
+ public int TraceQueueMaxDegreeOfParallelism { get; set; }
+ public TimeSpan TraceTimeout { get; set; }
+ public TimeSpan DefaultActionExecutionTimeout { get; set; }
+ public int ExecuteScriptLimitRecursion { get; set; }
+ public int ExecuteScriptMaxStatements { get; set; }
+ public long ExecuteScriptLimitMemory { get; set; }
+ public long ExecuteScriptLimitMemoryWarning { get; set; }
+ public TimeSpan ExecuteScriptTimeout { get; set; }
+ public TimeSpan ExecuteScriptV2Timeout { get; set; }
+ public int MaximumInputExpirationLoop { get; set; }
+ public long ExecuteScriptV2MaxRuntimeHeapSize { get; set; }
+ public long ExecuteScriptV2MaxRuntimeStackUsage { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Take.Blip.Builder.UnitTests/Actions/ExecuteScriptActionTests.cs b/src/Take.Blip.Builder.UnitTests/Actions/ExecuteScriptActionTests.cs
index 69b6d62a..4d928143 100644
--- a/src/Take.Blip.Builder.UnitTests/Actions/ExecuteScriptActionTests.cs
+++ b/src/Take.Blip.Builder.UnitTests/Actions/ExecuteScriptActionTests.cs
@@ -1,7 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using Jint.Runtime;
using Newtonsoft.Json.Linq;
@@ -42,6 +39,49 @@ public async Task ExecuteWithSingleStatementScriptShouldSucceed()
await Context.Received(1).SetVariableAsync(variableName, variableValue, CancellationToken, default(TimeSpan));
}
+ [Fact]
+ public async Task ExecuteWithDefaultTimeZoneShouldWork()
+ {
+ // Arrange
+ var settings = new ExecuteScriptSettings
+ {
+ // Fixed date to test timezone
+ Source = "function run() { return new Date('2021-01-01T00:00:10').toLocaleString('en-US'); }",
+ OutputVariable = "test"
+ };
+ var target = GetTarget();
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ // Jint doesn't support toLocaleString, so it will return the default date format
+ await Context.Received(1).SetVariableAsync("test", "Thursday, 31 December 2020 21:00:10", CancellationToken);
+ }
+
+ [Fact]
+ public async Task ExecuteWithCustomTimeZoneShouldWork()
+ {
+ // Arrange
+ var settings = new ExecuteScriptSettings
+ {
+ // Fixed date to test timezone
+ Source = "function run() { return new Date('2021-01-01T00:00:10').toLocaleString('en-US'); }",
+ OutputVariable = "test",
+ LocalTimeZoneEnabled = true
+ };
+ var target = GetTarget();
+
+ Context.Flow.Configuration["builder:#localTimeZone"] = "Asia/Shanghai";
+
+ // Act
+ await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
+
+ // Assert
+ // Jint doesn't support toLocaleString, so it will return the default date format
+ await Context.Received(1).SetVariableAsync("test", "Friday, 1 January 2021 08:00:10", CancellationToken);
+ }
+
[Fact]
public async Task ExecuteWithArgumentsShouldSucceed()
{
@@ -237,7 +277,7 @@ function run() {
[Fact]
public async Task ExecuteWithWhileTrueShouldFail()
{
- // Arrange
+ // Arrange
var result = "";
var settings = new ExecuteScriptSettings()
{
@@ -254,7 +294,7 @@ function run() {
};
var target = GetTarget();
- // Act
+ // Act
try
{
await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);
diff --git a/src/Take.Blip.Builder.UnitTests/Take.Blip.Builder.UnitTests.csproj b/src/Take.Blip.Builder.UnitTests/Take.Blip.Builder.UnitTests.csproj
index 3b48d43d..e8792f61 100644
--- a/src/Take.Blip.Builder.UnitTests/Take.Blip.Builder.UnitTests.csproj
+++ b/src/Take.Blip.Builder.UnitTests/Take.Blip.Builder.UnitTests.csproj
@@ -2,6 +2,7 @@
netcoreapp3.1;net6.0;net8.0
8.0
+ true
diff --git a/src/Take.Blip.Builder/Actions/ExecuteScriptV2/ExecuteScriptV2Action.cs b/src/Take.Blip.Builder/Actions/ExecuteScriptV2/ExecuteScriptV2Action.cs
new file mode 100644
index 00000000..59b4acc8
--- /dev/null
+++ b/src/Take.Blip.Builder/Actions/ExecuteScriptV2/ExecuteScriptV2Action.cs
@@ -0,0 +1,178 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Lime.Protocol;
+using Microsoft.ClearScript;
+using Microsoft.ClearScript.V8;
+using Serilog;
+using Take.Blip.Builder.Actions.ExecuteScriptV2.Functions;
+using Take.Blip.Builder.Hosting;
+using Take.Blip.Builder.Utils;
+
+namespace Take.Blip.Builder.Actions.ExecuteScriptV2
+{
+ ///
+ public class ExecuteScriptV2Action : ActionBase
+ {
+ private readonly IConfiguration _configuration;
+ private readonly IHttpClient _httpClient;
+ private readonly ILogger _logger;
+
+ ///
+ public ExecuteScriptV2Action(
+ IConfiguration configuration,
+ IHttpClient httpClient,
+ ILogger logger)
+ : base(nameof(ExecuteScriptV2))
+ {
+ HostSettings.CustomAttributeLoader = new LowerCaseMembersLoader();
+
+ _configuration = configuration;
+ _httpClient = httpClient;
+ _logger = logger;
+ }
+
+ ///
+ public override async Task ExecuteAsync(IContext context, ExecuteScriptV2Settings settings,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ var arguments = await GetScriptArgumentsAsync(context, settings, cancellationToken);
+
+ using var engine = new V8ScriptEngine(
+ V8ScriptEngineFlags.AddPerformanceObject |
+ V8ScriptEngineFlags.EnableTaskPromiseConversion |
+ V8ScriptEngineFlags.UseSynchronizationContexts |
+ V8ScriptEngineFlags.EnableStringifyEnhancements |
+ V8ScriptEngineFlags.EnableDateTimeConversion |
+ V8ScriptEngineFlags.EnableValueTaskPromiseConversion |
+ V8ScriptEngineFlags.HideHostExceptions
+ );
+
+ engine.DocumentSettings.AccessFlags |= DocumentAccessFlags.AllowCategoryMismatch;
+ engine.MaxRuntimeHeapSize =
+ new UIntPtr((ulong)_configuration.ExecuteScriptV2MaxRuntimeHeapSize);
+ engine.MaxRuntimeStackUsage =
+ new UIntPtr((ulong)_configuration.ExecuteScriptV2MaxRuntimeStackUsage);
+ engine.AllowReflection = false;
+
+ engine.RuntimeHeapSizeViolationPolicy = V8RuntimeViolationPolicy.Exception;
+
+ // Create new token cancellation token with _configuration.ExecuteScriptV2Timeout based on the current token
+ using var timeoutToken =
+ new CancellationTokenSource(_configuration.ExecuteScriptV2Timeout);
+ using var linkedToken =
+ CancellationTokenSource.CreateLinkedTokenSource(cancellationToken,
+ timeoutToken.Token);
+
+ var time = new Time(_logger, context, settings, linkedToken.Token);
+
+ engine.RegisterFunctions(settings, _httpClient, context, time, _logger,
+ linkedToken.Token);
+
+ var result = engine.ExecuteInvoke(settings.Source, settings.Function,
+ _configuration.ExecuteScriptV2Timeout, arguments);
+
+ await SetScriptResultAsync(context, settings, result, time, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ if (!settings.CaptureExceptions)
+ {
+ throw;
+ }
+
+ string exceptionMessage = null;
+
+ try
+ {
+ exceptionMessage =
+ await _captureException(context, settings, cancellationToken, ex);
+ }
+ finally
+ {
+ var trace = context.GetCurrentActionTrace();
+ if (trace != null)
+ {
+ trace.Warning = exceptionMessage ??
+ "An error occurred while executing the script.";
+ }
+ }
+ }
+ }
+
+ private async Task _captureException(IContext context,
+ ExecuteScriptV2Settings settings,
+ CancellationToken cancellationToken, Exception ex)
+ {
+ string exceptionMessage;
+
+ if (ex is ScriptEngineException ||
+ ex is ScriptInterruptedException ||
+ ex is TimeoutException ||
+ ex is ArgumentException ||
+ ex is ValidationException ||
+ ex is OperationCanceledException)
+ {
+ exceptionMessage = ex.Message;
+ }
+ else
+ {
+ var traceId = Activity.Current?.Id ?? Guid.NewGuid().ToString();
+
+ exceptionMessage =
+ $"Internal script error, please contact the support with the following id: {traceId}";
+
+ _logger.Error(ex, "Internal unknown bot error, support trace id: {TraceId}",
+ traceId);
+ }
+
+ if (!settings.ExceptionVariable.IsNullOrEmpty())
+ {
+ await context.SetVariableAsync(settings.ExceptionVariable,
+ exceptionMessage,
+ cancellationToken);
+ }
+
+ return exceptionMessage;
+ }
+
+ private static async Task