Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using Lime.Protocol.Serialization;
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;
using Take.Blip.Client;

namespace Take.Blip.Builder.Benchmark.Actions
{
Expand Down Expand Up @@ -41,7 +43,8 @@ public void Setup()
configuration.ExecuteScriptMaxStatements = 0;

_v2Action = new ExecuteScriptV2Action(configuration, Substitute.For<IHttpClient>(),
Substitute.For<ILogger>());
Substitute.For<ILogger>(), Substitute.For<ISender>(),
Substitute.For<IEnvelopeSerializer>());

_v1Action = new ExecuteScriptAction(configuration, Substitute.For<ILogger>());
}
Expand Down
156 changes: 154 additions & 2 deletions src/Take.Blip.Builder.UnitTests/Actions/ExecuteScript2ActionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,26 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Lime.Protocol;
using Lime.Protocol.Serialization;
using Microsoft.ClearScript;
using Newtonsoft.Json.Linq;
using NSubstitute;
using Serilog;
using Shouldly;
using Take.Blip.Builder.Actions;
using Take.Blip.Builder.Actions.ExecuteScriptV2;
using Take.Blip.Builder.Hosting;
using Take.Blip.Builder.Utils;
using Take.Blip.Client;
using Xunit;

namespace Take.Blip.Builder.UnitTests.Actions
{
public class ExecuteScript2ActionTests : ActionTestsBase
{
private static ExecuteScriptV2Action GetTarget(IHttpClient client = null)
private static ExecuteScriptV2Action GetTarget(IHttpClient client = null,
ISender sender = null, IEnvelopeSerializer envelopeSerializer = null)
{
var configuration = new TestConfiguration();
var conventions = new ConventionsConfiguration();
Expand All @@ -33,7 +38,8 @@ private static ExecuteScriptV2Action GetTarget(IHttpClient client = null)
conventions.ExecuteScriptV2MaxRuntimeStackUsage;

return new ExecuteScriptV2Action(configuration, client ?? Substitute.For<IHttpClient>(),
Substitute.For<ILogger>());
Substitute.For<ILogger>(), sender ?? Substitute.For<ISender>(),
envelopeSerializer ?? Substitute.For<IEnvelopeSerializer>());
}

[Fact]
Expand Down Expand Up @@ -1053,6 +1059,152 @@ await Context.Received(1).SetVariableAsync(Arg.Any<string>(), Arg.Any<string>(),
await Context.Received(1).SetVariableAsync("result", result, CancellationToken);
}

[Fact]
public async Task ExecuteScriptWithProcessCommandShouldSucceed()
{
// Arrange
var responseCommand = new Command()
{
Method = CommandMethod.Get,
Status = CommandStatus.Success
};

var sender = Substitute.For<ISender>();
sender.ProcessCommandAsync(Arg.Any<Command>(), Arg.Any<CancellationToken>())
.Returns(responseCommand);

var envelopeSerializer = LimeSerializerContainer.EnvelopeSerializer;

var expectedResult = envelopeSerializer.Serialize(responseCommand);

var settings = new ExecuteScriptV2Settings
{
Source = @"
async function run() {
var result = await command.processAsync({ method: 'get', uri: '/ping' });
return result;
}",
OutputVariable = "result"
};

var target = GetTarget(sender: sender, envelopeSerializer: envelopeSerializer);

// Act
await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);

// Assert
await sender.Received(1).ProcessCommandAsync(
Arg.Is<Command>(c => c.Uri.ToString() == "/ping" && c.Method == CommandMethod.Get),
Arg.Any<CancellationToken>());

await Context.Received(1).SetVariableAsync("result", expectedResult, CancellationToken);
}

[Fact]
public async Task ExecuteScriptWithProcessCommandNullPayloadShouldFail()
{
// Arrange
var sender = Substitute.For<ISender>();
var envelopeSerializer = LimeSerializerContainer.EnvelopeSerializer;

var settings = new ExecuteScriptV2Settings
{
Source = @"
async function run() {
var result = await command.processAsync(null);
return result;
}",
OutputVariable = "result",
CaptureExceptions = true,
ExceptionVariable = "exception"
};

var target = GetTarget(sender: sender, envelopeSerializer: envelopeSerializer);

// Act
await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);

// Assert
await sender.Received(0).ProcessCommandAsync(Arg.Any<Command>(), Arg.Any<CancellationToken>());
await Context.Received(1).SetVariableAsync(
Arg.Is<string>(s => s == "exception"),
Arg.Any<string>(),
Arg.Any<CancellationToken>());
}

[Fact]
public async Task ExecuteScriptWithProcessCommandInvalidPayloadShouldFail()
{
// Arrange
var sender = Substitute.For<ISender>();
var envelopeSerializer = LimeSerializerContainer.EnvelopeSerializer;

var settings = new ExecuteScriptV2Settings
{
Source = @"
async function run() {
var result = await command.processAsync('invalid-json{{}');
return result;
}",
OutputVariable = "result",
CaptureExceptions = true,
ExceptionVariable = "exception"
};

var target = GetTarget(sender: sender, envelopeSerializer: envelopeSerializer);

// Act
await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);

// Assert
await sender.Received(0).ProcessCommandAsync(Arg.Any<Command>(), Arg.Any<CancellationToken>());
await Context.Received(1).SetVariableAsync(
Arg.Is<string>(s => s == "exception"),
Arg.Any<string>(),
Arg.Any<CancellationToken>());
}

[Fact]
public async Task ExecuteScriptWithProcessCommandAndReturnResultShouldSucceed()
{
// Arrange
var responseCommand = new Command()
{
Method = CommandMethod.Set,
Status = CommandStatus.Success,
Uri = new LimeUri("/contacts")
};

var sender = Substitute.For<ISender>();
sender.ProcessCommandAsync(Arg.Any<Command>(), Arg.Any<CancellationToken>())
.Returns(responseCommand);

var envelopeSerializer = LimeSerializerContainer.EnvelopeSerializer;

var settings = new ExecuteScriptV2Settings
{
Source = @"
async function run() {
var resultJson = await command.processAsync({ method: 'set', uri: '/contacts', resource: { identity: 'user@domain.com' } });
var result = JSON.parse(resultJson);
return result.status;
}",
OutputVariable = "status"
};

var target = GetTarget(sender: sender, envelopeSerializer: envelopeSerializer);

// Act
await target.ExecuteAsync(Context, JObject.FromObject(settings), CancellationToken);

// Assert
await sender.Received(1).ProcessCommandAsync(
Arg.Is<Command>(c => c.Uri.ToString() == "/contacts" && c.Method == CommandMethod.Set),
Arg.Any<CancellationToken>());

await Context.Received(1).SetVariableAsync("status", "success", CancellationToken);
}

[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
private class TestConfiguration : IConfiguration
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
using System.Threading;
using System.Threading.Tasks;
using Lime.Protocol;
using Lime.Protocol.Serialization;
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;
using Take.Blip.Client;

namespace Take.Blip.Builder.Actions.ExecuteScriptV2
{
Expand All @@ -19,21 +21,27 @@ public class ExecuteScriptV2Action : ActionBase<ExecuteScriptV2Settings>
private readonly IConfiguration _configuration;
private readonly IHttpClient _httpClient;
private readonly ILogger _logger;
private readonly ISender _sender;
private readonly IEnvelopeSerializer _envelopeSerializer;

private static readonly string[] OUTPUT_PARAMETERS_NAME = new string[] { nameof(ExecuteScriptV2Settings.OutputVariable).ToCamelCase() };

/// <inheritdoc />
public ExecuteScriptV2Action(
IConfiguration configuration,
IHttpClient httpClient,
ILogger logger)
ILogger logger,
ISender sender,
IEnvelopeSerializer envelopeSerializer)
Comment on lines 30 to +35
: base(nameof(ExecuteScriptV2), OUTPUT_PARAMETERS_NAME)
{
HostSettings.CustomAttributeLoader = new LowerCaseMembersLoader();

_configuration = configuration;
_httpClient = httpClient;
_logger = logger;
_sender = sender;
_envelopeSerializer = envelopeSerializer;
}

/// <inheritdoc />
Expand Down Expand Up @@ -73,7 +81,7 @@ public override async Task ExecuteAsync(IContext context, ExecuteScriptV2Setting
var time = new Time(_logger, context, settings, linkedToken.Token);

engine.RegisterFunctions(settings, _httpClient, context, time, _logger,
linkedToken.Token);
_sender, _envelopeSerializer, _configuration, linkedToken.Token);

var result = engine.ExecuteInvoke(settings.Source, settings.Function,
_configuration.ExecuteScriptV2Timeout, arguments);
Expand Down
129 changes: 129 additions & 0 deletions src/Take.Blip.Builder/Actions/ExecuteScriptV2/Functions/Command.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Lime.Protocol.Serialization;
using Newtonsoft.Json.Linq;
using Serilog;
using Take.Blip.Builder.Hosting;
using Take.Blip.Client;
using LimeCommand = Lime.Protocol.Command;

namespace Take.Blip.Builder.Actions.ExecuteScriptV2.Functions
{
/// <summary>
/// Add Blip command functions to the script engine, allowing users to process LIME commands inside the JavaScript.
/// </summary>
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public class Command
{
private readonly ISender _sender;
private readonly IEnvelopeSerializer _envelopeSerializer;
private readonly IConfiguration _configuration;
private readonly IContext _context;
private readonly Time _time;
private readonly ILogger _logger;
private readonly CancellationToken _cancellationToken;

private const string SERIALIZABLE_PATTERN = @".+[/|\+]json$";

/// <summary>
/// Initializes a new instance of the <see cref="Command"/> class.
/// </summary>
/// <param name="sender"></param>
/// <param name="envelopeSerializer"></param>
/// <param name="configuration"></param>
/// <param name="context"></param>
/// <param name="time"></param>
/// <param name="logger"></param>
/// <param name="cancellationToken"></param>
public Command(
ISender sender,
IEnvelopeSerializer envelopeSerializer,
IConfiguration configuration,
IContext context,
Time time,
ILogger logger,
CancellationToken cancellationToken)
{
_sender = sender;
_envelopeSerializer = envelopeSerializer;
_configuration = configuration;
_context = context;
_time = time;
_logger = logger.ForContext("OwnerIdentity", context.OwnerIdentity)
.ForContext("UserIdentity", context.UserIdentity);
_cancellationToken = cancellationToken;
}

/// <summary>
/// Processes a Blip command and returns the serialized result.
/// </summary>
/// <param name="payload">The command payload as a JavaScript object.</param>
/// <returns>The serialized command response as a JSON string.</returns>
public async Task<string> ProcessAsync(object payload)
{
if (payload == null)
throw new ArgumentNullException(nameof(payload), "The command payload is required");

_logger.Information("[{Source}] Processing command", "ExecuteScriptV2.Command");

var jsonString = await ScriptObjectConverter.ToStringAsync(payload, _time, _cancellationToken);

if (string.IsNullOrWhiteSpace(jsonString))
throw new ArgumentException("The command payload cannot be empty", nameof(payload));

JObject settings;
try
{
settings = JObject.Parse(jsonString);
}
catch (Exception ex)
{
throw new ArgumentException($"Invalid command payload: {ex.Message}", nameof(payload), ex);
}

var command = _convertToCommand(settings);
command.Id = Lime.Protocol.EnvelopeId.NewId();

var resultCommand = await _sender.ProcessCommandAsync(command, _cancellationToken);
Comment on lines +89 to +92

return _envelopeSerializer.Serialize(resultCommand);
}

private LimeCommand _convertToCommand(JObject settings)
{
if (settings.TryGetValue(LimeCommand.TYPE_KEY, out var type)
&& Regex.IsMatch(type.ToString(), SERIALIZABLE_PATTERN, default, Constants.REGEX_TIMEOUT)
&& settings.TryGetValue(LimeCommand.RESOURCE_KEY, out var resource))
Comment on lines +97 to +101
{
settings.Property(LimeCommand.RESOURCE_KEY).Value = JObject.Parse(resource.ToString());
}

var command = settings.ToObject<LimeCommand>(LimeSerializerContainer.Serializer);
_insertMetadatasOnCommand(command);

return command;
}

private void _insertMetadatasOnCommand(LimeCommand command)
{
if (_configuration.ProcessCommandMetadatasToInsert != null &&
_configuration.ProcessCommandMetadatasToInsert.Count > 0)
{
if (command.Metadata is null)
command.Metadata = new Dictionary<string, string>();

var result = command.Metadata
.Concat(_configuration.ProcessCommandMetadatasToInsert)
.GroupBy(kv => kv.Key)
.ToDictionary(k => k.Key, v => v.Last().Value);

command.Metadata = result;
}
}
}
}
Loading
Loading