Browse Source

Feature/scriptingv2 (#495)

* Scripting extensions

* Scripting improvements.

* Scripting engine fixed.
pull/498/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
2603317cfe
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  2. 50
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Scripting/EventScriptExtension.cs
  3. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs
  4. 47
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs
  5. 37
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeScriptExtension.cs
  6. 111
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpScriptExtension.cs
  7. 63
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringScriptExtension.cs
  8. 12
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs
  9. 22
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptExtension.cs
  10. 54
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs
  11. 96
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHelpers.cs
  12. 98
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHttp.cs
  13. 296
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  14. 109
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs
  15. 53
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContextExtensions.cs
  16. 12
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  17. 12
      backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs
  18. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  19. 22
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  20. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs
  21. 20
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs
  22. 12
      backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs
  23. 10
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  24. 5
      backend/src/Squidex/Config/Domain/RuleServices.cs
  25. 20
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  26. 231
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  27. 372
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs
  28. 50
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs
  29. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  30. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  31. 24
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs
  32. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  33. 40
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  34. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs
  35. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs
  36. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs

7
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -90,13 +90,12 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{ {
var script = trimmed.Substring(ScriptPrefix.Length, trimmed.Length - ScriptPrefix.Length - ScriptSuffix.Length); var script = trimmed.Substring(ScriptPrefix.Length, trimmed.Length - ScriptPrefix.Length - ScriptSuffix.Length);
var customFunctions = new Dictionary<string, Func<string>> var context = new ScriptContext
{ {
["contentUrl"] = () => ContentUrl(@event), ["event"] = @event
["contentAction"] = () => ContentAction(@event)
}; };
return scriptEngine.Interpolate("event", @event, script, customFunctions); return scriptEngine.Interpolate(context, script);
} }
var current = text.AsSpan(); var current = text.AsSpan();

50
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Scripting/EventScriptExtension.cs

@ -0,0 +1,50 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint.Native;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.HandleRules.Scripting
{
public sealed class EventScriptExtension : IScriptExtension
{
private delegate JsValue EventDelegate();
private readonly IUrlGenerator urlGenerator;
public EventScriptExtension(IUrlGenerator urlGenerator)
{
Guard.NotNull(urlGenerator);
this.urlGenerator = urlGenerator;
}
public void Extend(ExecutionContext context, bool async)
{
context.Engine.SetValue("contentAction", new EventDelegate(() =>
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent)
{
return contentEvent.Status.ToString();
}
return JsValue.Null;
}));
context.Engine.SetValue("contentUrl", new EventDelegate(() =>
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedContentEvent contentEvent)
{
return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id);
}
return JsValue.Null;
}));
}
}
}

3
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs

@ -44,6 +44,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
case ClaimsPrincipal principal: case ClaimsPrincipal principal:
result = JintUser.Create(engine, principal); result = JintUser.Create(engine, principal);
return true; return true;
case Guid guid:
result = guid.ToString();
return true;
case Instant instant: case Instant instant:
result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); result = JsValue.FromObject(engine, instant.ToDateTimeUtc());
return true; return true;

47
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs

@ -0,0 +1,47 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using Jint;
namespace Squidex.Domain.Apps.Core.Scripting
{
public delegate bool ExceptionHandler(Exception exception);
public sealed class ExecutionContext : Dictionary<string, object>
{
private readonly ExceptionHandler? exceptionHandler;
public Engine Engine { get; }
public CancellationToken CancellationToken { get; }
internal ExecutionContext(Engine engine, CancellationToken cancellationToken, ExceptionHandler? exceptionHandler = null)
: base(StringComparer.OrdinalIgnoreCase)
{
Engine = engine;
CancellationToken = cancellationToken;
this.exceptionHandler = exceptionHandler;
}
public void Fail(Exception exception)
{
exceptionHandler?.Invoke(exception);
}
public ExecutionContext SetValue(string key, object value)
{
this[key] = value;
return this;
}
}
}

37
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/DateTimeScriptExtension.cs

@ -0,0 +1,37 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Globalization;
using Jint;
using Jint.Native;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{
public sealed class DateTimeScriptExtension : IScriptExtension
{
private delegate JsValue FormatDateDelegate(DateTime date, string format);
public void Extend(Engine engine)
{
engine.SetValue("formatTime", new FormatDateDelegate(FormatDate));
engine.SetValue("formatDate", new FormatDateDelegate(FormatDate));
}
private static JsValue FormatDate(DateTime date, string format)
{
try
{
return date.ToString(format, CultureInfo.InvariantCulture);
}
catch
{
return JsValue.Undefined;
}
}
}
}

111
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpScriptExtension.cs

@ -0,0 +1,111 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Jint.Native;
using Jint.Native.Json;
using Jint.Runtime;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{
public sealed class HttpScriptExtension : IScriptExtension
{
private delegate void GetJsonDelegate(string url, Action<JsValue> callback, JsValue? headers = null);
private readonly IHttpClientFactory httpClientFactory;
public HttpScriptExtension(IHttpClientFactory httpClientFactory)
{
Guard.NotNull(httpClientFactory);
this.httpClientFactory = httpClientFactory;
}
public void Extend(ExecutionContext context, bool async)
{
if (async)
{
var engine = context.Engine;
engine.SetValue("getJSON", new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers)));
}
}
private void GetJson(ExecutionContext context, string url, Action<JsValue> callback, JsValue? headers)
{
GetJsonAsync(context, url, callback, headers).Forget();
}
private async Task GetJsonAsync(ExecutionContext context, string url, Action<JsValue> callback, JsValue? headers)
{
try
{
using (var httpClient = httpClientFactory.CreateClient())
{
var request = CreateRequest(url, headers);
var response = await httpClient.SendAsync(request, context.CancellationToken);
response.EnsureSuccessStatusCode();
var responseObject = await ParseResponse(context, response);
context.Engine.ResetTimeoutTicks();
callback(responseObject);
}
}
catch (Exception ex)
{
context.Fail(ex);
}
}
private static HttpRequestMessage CreateRequest(string url, JsValue? headers)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
throw new ArgumentException("Url must be an absolute URL");
}
var request = new HttpRequestMessage(HttpMethod.Get, uri);
if (headers != null && headers.Type == Types.Object)
{
var obj = headers.AsObject();
foreach (var (key, property) in obj.GetOwnProperties())
{
var value = TypeConverter.ToString(property.Value);
if (!string.IsNullOrWhiteSpace(key))
{
request.Headers.TryAddWithoutValidation(key, value ?? string.Empty);
}
}
}
return request;
}
private async Task<JsValue> ParseResponse(ExecutionContext context, HttpResponseMessage response)
{
var responseString = await response.Content.ReadAsStringAsync();
context.CancellationToken.ThrowIfCancellationRequested();
var jsonParser = new JsonParser(context.Engine);
var jsonValue = jsonParser.Parse(responseString);
context.CancellationToken.ThrowIfCancellationRequested();
return jsonValue;
}
}
}

63
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringScriptExtension.cs

@ -0,0 +1,63 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
using Jint.Native;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting.Extensions
{
public sealed class StringScriptExtension : IScriptExtension
{
private delegate JsValue StringSlugifyDelegate(string text, bool single = false);
private delegate JsValue StringFormatDelegate(string text);
public void Extend(Engine engine)
{
engine.SetValue("slugify", new StringSlugifyDelegate(Slugify));
engine.SetValue("toCamelCase", new StringFormatDelegate(ToCamelCase));
engine.SetValue("toPascalCase", new StringFormatDelegate(ToPascalCase));
}
private static JsValue Slugify(string text, bool single = false)
{
try
{
return text.Slugify(null, single);
}
catch
{
return JsValue.Undefined;
}
}
private static JsValue ToCamelCase(string text)
{
try
{
return text.ToCamelCase();
}
catch
{
return JsValue.Undefined;
}
}
private static JsValue ToPascalCase(string text)
{
try
{
return text.ToPascalCase();
}
catch
{
return JsValue.Undefined;
}
}
}
}

12
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -15,16 +13,16 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
public interface IScriptEngine public interface IScriptEngine
{ {
void Execute(ScriptContext context, string script); Task ExecuteAsync(ScriptContext context, string script);
NamedContentData ExecuteAndTransform(ScriptContext context, string script); Task<NamedContentData> ExecuteAndTransformAsync(ScriptContext context, string script);
NamedContentData Transform(ScriptContext context, string script); Task<NamedContentData> TransformAsync(ScriptContext context, string script);
Task<IJsonValue> GetAsync(ScriptContext context, string script); Task<IJsonValue> GetAsync(ScriptContext context, string script);
bool Evaluate(string name, object context, string script); bool Evaluate(ScriptContext context, string script);
string? Interpolate(string name, object context, string script, Dictionary<string, Func<string>>? customFormatters = null); string? Interpolate(ScriptContext context, string script);
} }
} }

22
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptExtension.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
namespace Squidex.Domain.Apps.Core.Scripting
{
public interface IScriptExtension
{
void Extend(Engine engine)
{
}
void Extend(ExecutionContext context, bool async)
{
}
}
}

54
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Internal/Parser.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Esprima;
using Esprima.Ast;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting.Internal
{
internal sealed class Parser
{
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(10);
private static readonly ParserOptions DefaultParserOptions = new ParserOptions
{
AdaptRegexp = true, Tolerant = true, Loc = true
};
private readonly IMemoryCache memoryCache;
public Parser(IMemoryCache memoryCache)
{
Guard.NotNull(memoryCache);
this.memoryCache = memoryCache;
}
public Program Parse(string script)
{
var key = Key(script);
if (!memoryCache.TryGetValue<Program>(key, out var program))
{
var parser = new JavaScriptParser(script, DefaultParserOptions);
program = parser.ParseProgram();
memoryCache.Set(key, program, Expiration);
}
return program;
}
private static string Key(string script)
{
return $"SCRIPT_{script}";
}
}
}

96
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHelpers.cs

@ -1,96 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Globalization;
using Jint;
using Jint.Native;
using Jint.Native.Date;
using Jint.Runtime;
using Jint.Runtime.Interop;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
internal static class JintHelpers
{
public static Engine AddHelpers(this Engine engine)
{
engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify));
engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate));
engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate));
return engine;
}
public static Engine AddFormatters(this Engine engine, Dictionary<string, Func<string>>? customFormatters = null)
{
if (customFormatters != null)
{
foreach (var (key, value) in customFormatters)
{
engine.SetValue(key, Safe(value));
}
}
engine.AddHelpers();
return engine;
}
private static Func<string> Safe(Func<string> func)
{
return () =>
{
try
{
return func();
}
catch
{
return "null";
}
};
}
private static JsValue Slugify(JsValue thisObject, JsValue[] arguments)
{
try
{
var stringInput = TypeConverter.ToString(arguments.At(0));
var single = false;
if (arguments.Length > 1)
{
single = TypeConverter.ToBoolean(arguments.At(1));
}
return stringInput.Slugify(null, single);
}
catch
{
return JsValue.Undefined;
}
}
private static JsValue FormatDate(JsValue thisObject, JsValue[] arguments)
{
try
{
var dateValue = ((DateInstance)arguments.At(0)).ToDateTime();
var dateFormat = TypeConverter.ToString(arguments.At(1));
return dateValue.ToString(dateFormat, CultureInfo.InvariantCulture);
}
catch
{
return JsValue.Undefined;
}
}
}
}

98
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintHttp.cs

@ -1,98 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Jint;
using Jint.Native;
using Jint.Native.Json;
using Jint.Runtime;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Core.Scripting
{
internal sealed class JintHttp
{
private delegate void GetJsonDelegate(string url, Action<JsValue> callback, JsValue? headers = null);
private readonly IHttpClientFactory httpClientFactory;
private readonly Action<Exception> exceptionHandler;
private readonly CancellationToken cancellationToken;
private JsonParser parser;
public JintHttp(IHttpClientFactory httpClientFactory, CancellationToken cancellationToken, Action<Exception> exceptionHandler)
{
this.httpClientFactory = httpClientFactory;
this.exceptionHandler = exceptionHandler;
this.cancellationToken = cancellationToken;
}
public Engine Add(Engine engine)
{
parser = new JsonParser(engine);
engine.SetValue("getJSON", new GetJsonDelegate(GetJson));
return engine;
}
private void GetJson(string url, Action<JsValue> callback, JsValue? headers)
{
GetJSONAsync(url, callback, headers).Forget();
}
private async Task GetJSONAsync(string url, Action<JsValue> callback, JsValue? headers)
{
try
{
using (var httpClient = httpClientFactory.CreateClient())
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
throw new ArgumentException("Url must be an absolute URL");
}
var request = new HttpRequestMessage(HttpMethod.Get, uri);
if (headers != null && headers.Type == Types.Object)
{
var obj = headers.AsObject();
foreach (var (key, property) in obj.GetOwnProperties())
{
var value = TypeConverter.ToString(property.Value);
if (!string.IsNullOrWhiteSpace(key))
{
request.Headers.TryAddWithoutValidation(key, value ?? string.Empty);
}
}
}
var response = await httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
cancellationToken.ThrowIfCancellationRequested();
var responseString = await response.Content.ReadAsStringAsync();
cancellationToken.ThrowIfCancellationRequested();
var responseJson = parser.Parse(responseString);
callback(responseJson);
}
}
catch (Exception ex)
{
exceptionHandler(ex);
}
}
}
}

296
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -7,16 +7,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Esprima; using Esprima;
using Jint; using Jint;
using Jint.Native; using Jint.Native;
using Jint.Runtime; using Jint.Runtime;
using Jint.Runtime.Interop; using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Domain.Apps.Core.Scripting.Internal;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
@ -25,74 +26,73 @@ namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed class JintScriptEngine : IScriptEngine public sealed class JintScriptEngine : IScriptEngine
{ {
private readonly IHttpClientFactory? httpClientFactory; private readonly IScriptExtension[] extensions;
private readonly Parser parser;
public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200);
public JintScriptEngine(IHttpClientFactory? httpClientFactory = null) public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMilliseconds(4000);
{
this.httpClientFactory = httpClientFactory;
}
public void Execute(ScriptContext context, string script) public JintScriptEngine(IMemoryCache memoryCache, IEnumerable<IScriptExtension>? extensions = null)
{ {
Guard.NotNull(context); parser = new Parser(memoryCache);
if (!string.IsNullOrWhiteSpace(script))
{
var engine =
CreateScriptEngine()
.AddContext(context)
.AddDisallow()
.AddReject();
Execute(engine, script); this.extensions = extensions?.ToArray() ?? Array.Empty<IScriptExtension>();
}
} }
public NamedContentData ExecuteAndTransform(ScriptContext context, string script) public async Task ExecuteAsync(ScriptContext context, string script)
{ {
Guard.NotNull(context); Guard.NotNull(context);
Guard.NotNullOrEmpty(script);
var result = context.Data!; using (var cts = new CancellationTokenSource(ExecutionTimeout))
if (!string.IsNullOrWhiteSpace(script))
{ {
var engine = var tcs = new TaskCompletionSource<bool>();
CreateScriptEngine()
.AddContext(context)
.AddDisallow()
.AddReject();
engine.SetValue("replace", new Action(() => using (cts.Token.Register(() => tcs.TrySetCanceled()))
{ {
var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); var engine =
CreateEngine(context, true, cts.Token, tcs.TrySetException, true)
.AddDisallow()
.AddReject();
engine.SetValue("complete", new Action<JsValue?>(value =>
{
tcs.TrySetResult(true);
}));
Execute(engine, script);
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) if (engine.GetValue("async") != true)
{ {
data.TryUpdate(out result); tcs.TrySetResult(true);
} }
}));
Execute(engine, script); await tcs.Task;
}
} }
return result;
} }
public NamedContentData Transform(ScriptContext context, string script) public async Task<NamedContentData> ExecuteAndTransformAsync(ScriptContext context, string script)
{ {
Guard.NotNull(context); Guard.NotNull(context);
Guard.NotNullOrEmpty(script);
var result = context.Data!; using (var cts = new CancellationTokenSource(ExecutionTimeout))
if (!string.IsNullOrWhiteSpace(script))
{ {
try var tcs = new TaskCompletionSource<NamedContentData>();
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{ {
var engine = var engine =
CreateScriptEngine() CreateEngine(context, true, cts.Token, tcs.TrySetException, true)
.AddContext(context); .AddDisallow()
.AddReject();
engine.SetValue("complete", new Action<JsValue?>(value =>
{
tcs.TrySetResult(context.Data!);
}));
engine.SetValue("replace", new Action(() => engine.SetValue("replace", new Action(() =>
{ {
@ -100,70 +100,96 @@ namespace Squidex.Domain.Apps.Core.Scripting
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{ {
data.TryUpdate(out result); if (!tcs.Task.IsCompleted)
{
if (data.TryUpdate(out var modified))
{
tcs.TrySetResult(modified);
}
else
{
tcs.TrySetResult(context.Data!);
}
}
} }
})); }));
engine.Execute(script); Execute(engine, script);
}
catch (Exception)
{
result = context.Data!;
}
}
return result; if (engine.GetValue("async") != true)
} {
tcs.TrySetResult(context.Data!);
}
private static void Execute(Engine engine, string script) return await tcs.Task;
{ }
try
{
engine.Execute(script);
}
catch (ArgumentException ex)
{
throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message));
}
catch (JavaScriptException ex)
{
throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message));
}
catch (ParserException ex)
{
throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message));
} }
} }
private Engine CreateScriptEngine(IReferenceResolver? resolver = null) public async Task<NamedContentData> TransformAsync(ScriptContext context, string script)
{ {
var engine = new Engine(options => Guard.NotNull(context);
Guard.NotNullOrEmpty(script);
using (var cts = new CancellationTokenSource(ExecutionTimeout))
{ {
if (resolver != null) var tcs = new TaskCompletionSource<NamedContentData>();
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{ {
options.SetReferencesResolver(resolver); var engine = CreateEngine(context, true, cts.Token, tcs.TrySetException, true);
}
options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance); engine.SetValue("complete", new Action<JsValue?>(value =>
}); {
tcs.TrySetResult(context.Data!);
}));
engine.SetValue("replace", new Action(() =>
{
var dataInstance = engine.GetValue("ctx").AsObject().Get("data");
if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data)
{
if (!tcs.Task.IsCompleted)
{
if (data.TryUpdate(out var modified))
{
tcs.TrySetResult(modified);
}
else
{
tcs.TrySetResult(context.Data!);
}
}
}
}));
engine.AddHelpers(); Execute(engine, script);
if (engine.GetValue("async") != true)
{
tcs.TrySetResult(context.Data!);
}
return engine; return await tcs.Task;
}
}
} }
public bool Evaluate(string name, object context, string script) public bool Evaluate(ScriptContext context, string script)
{ {
Guard.NotNull(context);
Guard.NotNullOrEmpty(script);
try try
{ {
var result = var engine = CreateEngine(context, false);
CreateScriptEngine(NullPropagation.Instance)
.SetValue(name, context) Execute(engine, script);
.Execute(script)
.GetCompletionValue() var converted = Equals(engine.GetCompletionValue().ToObject(), true);
.ToObject();
return (bool)result; return converted;
} }
catch catch
{ {
@ -171,19 +197,18 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
} }
public string? Interpolate(string name, object context, string script, Dictionary<string, Func<string>>? customFormatters = null) public string? Interpolate(ScriptContext context, string script)
{ {
Guard.NotNull(context);
Guard.NotNullOrEmpty(script);
try try
{ {
var result = var engine = CreateEngine(context, false);
CreateScriptEngine(NullPropagation.Instance)
.AddFormatters(customFormatters) Execute(engine, script);
.SetValue(name, context)
.Execute(script)
.GetCompletionValue()
.ToObject();
var converted = result.ToString(); var converted = engine.GetCompletionValue().ToObject()?.ToString() ?? "null";
return converted == "undefined" ? "null" : converted; return converted == "undefined" ? "null" : converted;
} }
@ -195,36 +220,89 @@ namespace Squidex.Domain.Apps.Core.Scripting
public Task<IJsonValue> GetAsync(ScriptContext context, string script) public Task<IJsonValue> GetAsync(ScriptContext context, string script)
{ {
using (var cts = new CancellationTokenSource(Timeout)) Guard.NotNull(context);
Guard.NotNullOrEmpty(script);
using (var cts = new CancellationTokenSource(ExecutionTimeout))
{ {
var tcs = new TaskCompletionSource<IJsonValue>(); var tcs = new TaskCompletionSource<IJsonValue>();
using (cts.Token.Register(() => using (cts.Token.Register(() =>
{ {
tcs.SetCanceled(); tcs.TrySetCanceled();
})) }))
{ {
var engine = var engine = CreateEngine(context, true, cts.Token, ex => tcs.TrySetException(ex), true);
CreateScriptEngine()
.AddContext(context);
if (httpClientFactory != null)
{
var http = new JintHttp(httpClientFactory, cts.Token, tcs.SetException);
http.Add(engine);
}
engine.SetValue("complete", new Action<JsValue?>(value => engine.SetValue("complete", new Action<JsValue?>(value =>
{ {
tcs.SetResult(JsonMapper.Map(value)); tcs.TrySetResult(JsonMapper.Map(value));
})); }));
engine.Execute(script); engine.Execute(script);
if (engine.GetValue("async") != true)
{
tcs.TrySetResult(JsonMapper.Map(engine.GetCompletionValue()));
}
} }
return tcs.Task; return tcs.Task;
} }
} }
private Engine CreateEngine(ScriptContext context, bool nested, CancellationToken cancellationToken = default, ExceptionHandler? exceptionHandler = null, bool async = false)
{
var engine = new Engine(options =>
{
options.AddObjectConverter(DefaultConverter.Instance);
options.SetReferencesResolver(NullPropagation.Instance);
options.Strict();
options.TimeoutInterval(Timeout);
});
if (async)
{
engine.SetValue("async", false);
}
foreach (var extension in extensions)
{
extension.Extend(engine);
}
var executionContext = new ExecutionContext(engine, cancellationToken, exceptionHandler);
foreach (var extension in extensions)
{
extension.Extend(executionContext, async);
}
context.Add(executionContext, nested);
return executionContext.Engine;
}
private void Execute(Engine engine, string script)
{
try
{
var program = parser.Parse(script);
engine.Execute(program);
}
catch (ArgumentException ex)
{
throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message));
}
catch (JavaScriptException ex)
{
throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message));
}
catch (ParserException ex)
{
throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message));
}
}
} }
} }

109
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs

@ -6,25 +6,118 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Claims; using System.Security.Claims;
using Jint.Native;
using Jint.Native.Object;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting namespace Squidex.Domain.Apps.Core.Scripting
{ {
public sealed class ScriptContext public sealed class ScriptContext : Dictionary<string, object?>
{ {
public ClaimsPrincipal User { get; set; } public ScriptContext()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public Guid ContentId { get; set; } public ClaimsPrincipal? User
{
get => GetValue<ClaimsPrincipal?>();
set => SetValue(value);
}
public NamedContentData? Data { get; set; } public Guid ContentId
{
get => GetValue<Guid>();
set => SetValue(value);
}
public NamedContentData DataOld { get; set; } public NamedContentData? Data
{
get => GetValue<NamedContentData?>();
set => SetValue(value);
}
public Status Status { get; set; } public NamedContentData? DataOld
{
get => GetValue<NamedContentData?>("oldData");
set => SetValue(value, "oldData");
}
public Status StatusOld { get; set; } public Status Status
{
get => GetValue<Status>();
set => SetValue(value);
}
public string Operation { get; set; } public Status StatusOld
{
get => GetValue<Status>();
set => SetValue(value);
}
public string? Operation
{
get => GetValue<string?>();
set => SetValue(value);
}
public void SetValue(object? value, [CallerMemberNameAttribute] string? key = null)
{
if (key != null)
{
this[key] = value;
}
}
public T GetValue<T>([CallerMemberNameAttribute] string? key = null)
{
if (key != null && TryGetValue(key, out var temp) && temp is T result)
{
return result;
}
return default!;
}
internal void Add(ExecutionContext context, bool nested)
{
var engine = context.Engine;
if (nested)
{
var contextInstance = new ObjectInstance(engine);
foreach (var (key, value) in this)
{
var property = key.ToCamelCase();
if (value != null)
{
contextInstance.FastAddProperty(property, JsValue.FromObject(engine, value), true, true, true);
context[property] = value;
}
}
engine.SetValue("ctx", contextInstance);
engine.SetValue("context", contextInstance);
}
else
{
foreach (var (key, value) in this)
{
var property = key.ToCamelCase();
if (value != null)
{
engine.SetValue(property, value);
context[property] = value;
}
}
}
}
} }
} }

53
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContextExtensions.cs

@ -1,53 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Jint;
using Jint.Native.Object;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
namespace Squidex.Domain.Apps.Core.Scripting
{
internal static class ScriptContextExtensions
{
public static Engine AddContext(this Engine engine, ScriptContext context)
{
var contextInstance = new ObjectInstance(engine);
if (context.Data != null)
{
contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true);
}
if (context.DataOld != null)
{
contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.DataOld), true, true, true);
}
if (context.User != null)
{
contextInstance.FastAddProperty("user", JintUser.Create(engine, context.User), false, true, false);
}
if (!string.IsNullOrWhiteSpace(context.Operation))
{
contextInstance.FastAddProperty("operation", context.Operation, false, false, false);
}
contextInstance.FastAddProperty("status", context.Status.ToString(), false, false, false);
if (context.StatusOld != default)
{
contextInstance.FastAddProperty("oldStatus", context.StatusOld.ToString(), false, false, false);
}
engine.SetValue("ctx", contextInstance);
engine.SetValue("context", contextInstance);
return engine;
}
}
}

12
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -68,7 +68,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger) protected override bool Trigger(EnrichedAssetEvent @event, AssetChangedTriggerV2 trigger)
{ {
return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); if (string.IsNullOrWhiteSpace(trigger.Condition))
{
return true;
}
var context = new ScriptContext
{
["event"] = @event
};
return scriptEngine.Evaluate(context, trigger.Condition);
} }
} }
} }

12
backend/src/Squidex.Domain.Apps.Entities/Comments/CommentTriggerHandler.cs

@ -71,7 +71,17 @@ namespace Squidex.Domain.Apps.Entities.Comments
protected override bool Trigger(EnrichedCommentEvent @event, CommentTrigger trigger) protected override bool Trigger(EnrichedCommentEvent @event, CommentTrigger trigger)
{ {
return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); if (string.IsNullOrWhiteSpace(trigger.Condition))
{
return true;
}
var context = new ScriptContext
{
["event"] = @event
};
return scriptEngine.Evaluate(context, trigger.Condition);
} }
} }
} }

12
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs

@ -140,7 +140,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event) private bool MatchsCondition(ContentChangedTriggerSchemaV2 schema, EnrichedSchemaEventBase @event)
{ {
return string.IsNullOrWhiteSpace(schema.Condition) || scriptEngine.Evaluate("event", @event, schema.Condition); if (string.IsNullOrWhiteSpace(schema.Condition))
{
return true;
}
var context = new ScriptContext
{
["event"] = @event
};
return scriptEngine.Evaluate(context, schema.Condition);
} }
} }
} }

22
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -96,22 +96,32 @@ namespace Squidex.Domain.Apps.Entities.Contents
return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); return data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message);
} }
public Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptContext context) public async Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptContext context)
{ {
Enrich(context); Enrich(context);
var result = scriptEngine.ExecuteAndTransform(context, GetScript(script)); var actualScript = GetScript(script);
return Task.FromResult(result); if (string.IsNullOrWhiteSpace(actualScript))
{
return context.Data!;
}
return await scriptEngine.ExecuteAndTransformAsync(context, actualScript);
} }
public Task ExecuteScriptAsync(Func<SchemaScripts, string> script, ScriptContext context) public async Task ExecuteScriptAsync(Func<SchemaScripts, string> script, ScriptContext context)
{ {
Enrich(context); Enrich(context);
scriptEngine.Execute(context, GetScript(script)); var actualScript = GetScript(script);
return Task.CompletedTask; if (string.IsNullOrWhiteSpace(actualScript))
{
return;
}
await scriptEngine.ExecuteAsync(context, GetScript(script));
} }
private void Enrich(ScriptContext context) private void Enrich(ScriptContext context)

7
backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs

@ -115,7 +115,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (!string.IsNullOrWhiteSpace(condition?.Expression)) if (!string.IsNullOrWhiteSpace(condition?.Expression))
{ {
return scriptEngine.Evaluate("data", data, condition.Expression); var context = new ScriptContext
{
["data"] = data
};
return scriptEngine.Evaluate(context, condition.Expression);
} }
return true; return true;

20
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs

@ -38,20 +38,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
var results = new List<IEnrichedContentEntity>(); var results = new List<IEnrichedContentEntity>();
var scriptContext = new ScriptContext { User = context.User }; await Task.WhenAll(group.Select(x => TransformAsync(context, script, x)));
foreach (var content in group)
{
scriptContext.Data = content.Data;
scriptContext.ContentId = content.Id;
content.Data = scriptEngine.Transform(scriptContext, script);
}
} }
} }
} }
} }
private async Task TransformAsync(Context context, string script, ContentEntity content)
{
var scriptContext = new ScriptContext { User = context.User };
scriptContext.Data = content.Data;
scriptContext.ContentId = content.Id;
content.Data = await scriptEngine.TransformAsync(scriptContext, script);
}
private static bool ShouldEnrich(Context context) private static bool ShouldEnrich(Context context)
{ {
return !context.IsFrontendClient; return !context.IsFrontendClient;

12
backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaChangedTriggerHandler.cs

@ -71,7 +71,17 @@ namespace Squidex.Domain.Apps.Entities.Schemas
protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger) protected override bool Trigger(EnrichedSchemaEvent @event, SchemaChangedTrigger trigger)
{ {
return string.IsNullOrWhiteSpace(trigger.Condition) || scriptEngine.Evaluate("event", @event, trigger.Condition); if (string.IsNullOrWhiteSpace(trigger.Condition))
{
return true;
}
var context = new ScriptContext
{
["event"] = @event
};
return scriptEngine.Evaluate(context, trigger.Condition);
} }
} }
} }

10
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -16,6 +16,7 @@ using Squidex.Areas.Api.Controllers.News;
using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Areas.Api.Controllers.News.Service;
using Squidex.Areas.Api.Controllers.UI; using Squidex.Areas.Api.Controllers.UI;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Tags; using Squidex.Domain.Apps.Entities.Tags;
@ -58,6 +59,15 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<JintScriptEngine>() services.AddSingletonAs<JintScriptEngine>()
.AsOptional<IScriptEngine>(); .AsOptional<IScriptEngine>();
services.AddSingletonAs<DateTimeScriptExtension>()
.As<IScriptExtension>();
services.AddSingletonAs<StringScriptExtension>()
.As<IScriptExtension>();
services.AddSingletonAs<HttpScriptExtension>()
.As<IScriptExtension>();
services.AddSingleton<Func<IIncomingGrainCallContext, string>>(DomainObjectGrainFormatter.Format); services.AddSingleton<Func<IIncomingGrainCallContext, string>>(DomainObjectGrainFormatter.Format);
} }

5
backend/src/Squidex/Config/Domain/RuleServices.cs

@ -8,6 +8,8 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.Scripting;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
@ -64,6 +66,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<RuleRegistry>() services.AddSingletonAs<RuleRegistry>()
.As<ITypeProvider>().AsSelf(); .As<ITypeProvider>().AsSelf();
services.AddSingletonAs<EventScriptExtension>()
.As<IScriptExtension>();
services.AddSingletonAs<RuleEventFormatter>() services.AddSingletonAs<RuleEventFormatter>()
.AsSelf(); .AsSelf();

20
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -9,11 +9,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.Scripting;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
@ -46,7 +50,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId)) A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId))
.Returns("content-url"); .Returns("content-url");
sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine(null)); var extensions = new IScriptExtension[]
{
new DateTimeScriptExtension(),
new EventScriptExtension(urlGenerator),
new StringScriptExtension()
};
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
sut = new RuleEventFormatter(TestUtils.DefaultSerializer, urlGenerator, new JintScriptEngine(cache, extensions));
} }
[Fact] [Fact]
@ -187,6 +200,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory] [Theory]
[InlineData("$CONTENT_STATUS")] [InlineData("$CONTENT_STATUS")]
[InlineData("Script(contentAction())")]
[InlineData("Script(`${event.status}`)")] [InlineData("Script(`${event.status}`)")]
public void Should_format_content_status_when_found(string script) public void Should_format_content_status_when_found(string script)
{ {
@ -200,7 +214,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory] [Theory]
[InlineData("$CONTENT_ACTION")] [InlineData("$CONTENT_ACTION")]
[InlineData("Script(contentAction())")] [InlineData("Script(contentAction())")]
public void Should_null_when_content_status_not_found(string script) public void Should_return_null_when_content_status_not_found(string script)
{ {
var @event = new EnrichedAssetEvent(); var @event = new EnrichedAssetEvent();
@ -224,7 +238,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory] [Theory]
[InlineData("$CONTENT_ACTION")] [InlineData("$CONTENT_ACTION")]
[InlineData("Script(contentAction())")] [InlineData("Script(contentAction())")]
public void Should_null_when_content_action_not_found(string script) public void Should_return_null_when_content_action_not_found(string script)
{ {
var @event = new EnrichedAssetEvent(); var @event = new EnrichedAssetEvent();

231
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

@ -0,0 +1,231 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
public class JintScriptEngineHelperTests
{
private readonly IHttpClientFactory httpClientFactory = A.Fake<IHttpClientFactory>();
private readonly JintScriptEngine sut;
public JintScriptEngineHelperTests()
{
var extensions = new IScriptExtension[]
{
new DateTimeScriptExtension(),
new HttpScriptExtension(httpClientFactory),
new StringScriptExtension()
};
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
sut = new JintScriptEngine(cache, extensions)
{
Timeout = TimeSpan.FromSeconds(1)
};
}
[Fact]
public void Should_camel_case_value()
{
const string script = @"
return toCamelCase(value);
";
var context = new ScriptContext
{
["value"] = "Hello World"
};
var result = sut.Interpolate(context, script);
Assert.Equal("helloWorld", result);
}
[Fact]
public void Should_pascal_case_value()
{
const string script = @"
return toPascalCase(value);
";
var context = new ScriptContext
{
["value"] = "Hello World"
};
var result = sut.Interpolate(context, script);
Assert.Equal("HelloWorld", result);
}
[Fact]
public void Should_slugify_value()
{
const string script = @"
return slugify(value);
";
var context = new ScriptContext
{
["value"] = "4 Häuser"
};
var result = sut.Interpolate(context, script);
Assert.Equal("4-haeuser", result);
}
[Fact]
public void Should_slugify_value_with_single_char()
{
const string script = @"
return slugify(value, true);
";
var context = new ScriptContext
{
["value"] = "4 Häuser"
};
var result = sut.Interpolate(context, script);
Assert.Equal("4-hauser", result);
}
[Fact]
public async Task Should_throw_validation_exception_when_calling_reject()
{
const string script = @"
reject()
";
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script));
Assert.Empty(ex.Errors);
}
[Fact]
public async Task Should_throw_validation_exception_when_calling_reject_with_message()
{
const string script = @"
reject('Not valid')
";
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script));
Assert.Equal("Not valid", ex.Errors.Single().Message);
}
[Fact]
public async Task Should_throw_security_exception_when_calling_reject()
{
const string script = @"
disallow()
";
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptContext(), script));
Assert.Equal("Not allowed", ex.Message);
}
[Fact]
public async Task Should_throw_security_exception_when_calling_reject_with_message()
{
const string script = @"
disallow('Operation not allowed')
";
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptContext(), script));
Assert.Equal("Operation not allowed", ex.Message);
}
[Fact]
public async Task Should_make_json_request()
{
var httpHandler = SetupRequest();
const string script = @"
async = true;
getJSON('http://squidex.io', function(result) {
complete(result);
});
";
var result = await sut.GetAsync(new ScriptContext(), script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/");
var expectedResult = JsonValue.Object().Add("key", 42);
Assert.Equal(expectedResult, result);
}
[Fact]
public async Task Should_make_json_request_with_headers()
{
var httpHandler = SetupRequest();
const string script = @"
async = true;
var headers = {
'X-Header1': 1,
'X-Header2': '2'
};
getJSON('http://squidex.io', function(result) {
complete(result);
}, headers);
";
var result = await sut.GetAsync(new ScriptContext(), script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/");
httpHandler.ShouldBeHeader("X-Header1", "1");
httpHandler.ShouldBeHeader("X-Header2", "2");
var expectedResult = JsonValue.Object().Add("key", 42);
Assert.Equal(expectedResult, result);
}
private MockupHttpHandler SetupRequest()
{
var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{ \"key\": 42 }")
};
var httpHandler = new MockupHttpHandler(httpResponse);
A.CallTo(() => httpClientFactory.CreateClient(A<string>._))
.Returns(new HttpClient(httpHandler));
return httpHandler;
}
}
}

372
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs

@ -6,17 +6,16 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Security.Claims; using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure; using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
using Xunit; using Xunit;
@ -30,135 +29,165 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public JintScriptEngineTests() public JintScriptEngineTests()
{ {
sut = new JintScriptEngine(httpClientFactory) var extensions = new IScriptExtension[]
{ {
Timeout = TimeSpan.FromSeconds(1) new DateTimeScriptExtension(),
new HttpScriptExtension(httpClientFactory),
new StringScriptExtension()
}; };
}
[Fact] var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)
public void Should_throw_validation_exception_when_calling_reject() {
{ Content = new StringContent("{ \"key\": 42 }")
const string script = @" };
reject()
"; var httpHandler = new MockupHttpHandler(httpResponse);
A.CallTo(() => httpClientFactory.CreateClient(A<string>._))
.Returns(new HttpClient(httpHandler));
var ex = Assert.Throws<ValidationException>(() => sut.Execute(new ScriptContext(), script)); var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
Assert.Empty(ex.Errors); sut = new JintScriptEngine(cache, extensions)
{
Timeout = TimeSpan.FromSeconds(1)
};
} }
[Fact] [Fact]
public void Should_throw_validation_exception_when_calling_reject_with_message() public async Task ExecuteAsync_should_catch_script_syntax_errors()
{ {
const string script = @" const string script = @"
reject('Not valid') invalid()
"; ";
var ex = Assert.Throws<ValidationException>(() => sut.Execute(new ScriptContext(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script));
Assert.Equal("Not valid", ex.Errors.Single().Message);
} }
[Fact] [Fact]
public void Should_throw_security_exception_when_calling_reject() public async Task ExecuteAsync_should_catch_script_runtime_errors()
{ {
const string script = @" const string script = @"
disallow() throw 'Error';
"; ";
var ex = Assert.Throws<DomainForbiddenException>(() => sut.Execute(new ScriptContext(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script));
Assert.Equal("Not allowed", ex.Message);
} }
[Fact] [Fact]
public void Should_throw_security_exception_when_calling_reject_with_message() public async Task TransformAsync_should_return_original_content_when_script_failed()
{ {
var content = new NamedContentData();
var context = new ScriptContext { Data = content };
const string script = @" const string script = @"
disallow('Operation not allowed') x => x
"; ";
var ex = Assert.Throws<DomainForbiddenException>(() => sut.Execute(new ScriptContext(), script)); var result = await sut.TransformAsync(context, script);
Assert.Equal("Operation not allowed", ex.Message); Assert.Empty(result);
} }
[Fact] [Fact]
public void Should_catch_script_syntax_errors() public async Task TransformAsync_should_transform_content()
{ {
const string script = @" var content =
invalid() new NamedContentData()
"; .AddField("number0",
new ContentFieldData()
.AddValue("iv", 1.0))
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 1.0));
var expected =
new NamedContentData()
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 2.0))
.AddField("number2",
new ContentFieldData()
.AddValue("iv", 10.0));
Assert.Throws<ValidationException>(() => sut.Execute(new ScriptContext(), script)); var context = new ScriptContext { Data = content };
}
[Fact]
public void Should_catch_script_runtime_errors()
{
const string script = @" const string script = @"
throw 'Error'; var data = ctx.data;
delete data.number0;
data.number1.iv = data.number1.iv + 1;
data.number2 = { 'iv': 10 };
replace(data);
"; ";
Assert.Throws<ValidationException>(() => sut.Execute(new ScriptContext(), script)); var result = await sut.TransformAsync(context, script);
Assert.Equal(expected, result);
} }
[Fact] [Fact]
public void Should_catch_script_runtime_errors_on_execute_and_transform() public async Task ExecuteAndTransformAsync_should_catch_javascript_error()
{ {
const string script = @" const string script = @"
throw 'Error'; throw 'Error';
"; ";
Assert.Throws<ValidationException>(() => sut.ExecuteAndTransform(new ScriptContext(), script)); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAndTransformAsync(new ScriptContext(), script));
} }
[Fact] [Fact]
public void Should_return_original_content_when_transform_script_failed() public async Task ExecuteAndTransformAsync_should_throw_when_script_failed()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content }; var context = new ScriptContext { Data = content };
const string script = @" const string script = @"
x => x invalid();
"; ";
var result = sut.Transform(context, script); await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAndTransformAsync(context, script));
Assert.Same(content, result);
} }
[Fact] [Fact]
public void Should_throw_when_execute_and_transform_script_failed() public async Task ExecuteAndTransformAsync_should_return_original_content_when_not_replaced()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content }; var context = new ScriptContext { Data = content };
const string script = @" const string script = @"
invalid(); var x = 0;
"; ";
Assert.Throws<ValidationException>(() => sut.ExecuteAndTransform(context, script)); var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Empty(result);
} }
[Fact] [Fact]
public void Should_return_original_content_when_content_is_not_replaced() public async Task ExecuteAndTransformAsync_should_return_original_content_when_not_replaced_async()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
var context = new ScriptContext { Data = content }; var context = new ScriptContext { Data = content };
const string script = @" const string script = @"
async = true;
var x = 0; var x = 0;
getJSON('http://squidex.io', function(result) {
complete();
});
"; ";
var result = sut.ExecuteAndTransform(context, script); var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Same(content, result); Assert.Empty(result);
} }
[Fact] [Fact]
public void Should_fetch_operation_name() public async Task ExecuteAndTransformAsync_should_transform_object()
{ {
var content = new NamedContentData(); var content = new NamedContentData();
@ -178,117 +207,86 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(data); replace(data);
"; ";
var result = sut.ExecuteAndTransform(context, script); var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }
[Fact] [Fact]
public void Should_transform_content_and_return_with_transform() public async Task ExecuteAndTransformAsync_should_transform_object_async()
{ {
var content = var content = new NamedContentData();
new NamedContentData()
.AddField("number0",
new ContentFieldData()
.AddValue("iv", 1.0))
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 1.0));
var expected = var expected =
new NamedContentData() new NamedContentData()
.AddField("number1", .AddField("operation",
new ContentFieldData()
.AddValue("iv", 2.0))
.AddField("number2",
new ContentFieldData() new ContentFieldData()
.AddValue("iv", 10.0)); .AddValue("iv", 42));
var context = new ScriptContext { Data = content }; var context = new ScriptContext { Data = content, Operation = "MyOperation" };
const string script = @" const string script = @"
async = true;
var data = ctx.data; var data = ctx.data;
delete data.number0; getJSON('http://squidex.io', function(result) {
data.operation = { iv: result.key };
data.number1.iv = data.number1.iv + 1; replace(data);
data.number2 = { 'iv': 10 }; });
replace(data);
"; ";
var result = sut.Transform(context, script); var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }
[Fact] [Fact]
public void Should_slugify_value() public async Task ExecuteAndTransformAsync_should_ignore_transformation_when_async_not_set()
{ {
var content = var content = new NamedContentData();
new NamedContentData() var context = new ScriptContext { Data = content, Operation = "MyOperation" };
.AddField("title",
new ContentFieldData()
.AddValue("iv", "4 Häuser"));
var expected =
new NamedContentData()
.AddField("title",
new ContentFieldData()
.AddValue("iv", "4 Häuser"))
.AddField("slug",
new ContentFieldData()
.AddValue("iv", "4-haeuser"));
var context = new ScriptContext { Data = content };
const string script = @" const string script = @"
var data = ctx.data; var data = ctx.data;
data.slug = { iv: slugify(data.title.iv) }; getJSON('http://squidex.io', function(result) {
data.operation = { iv: result.key };
replace(data);
});
replace(data);
"; ";
var result = sut.Transform(context, script); var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result); Assert.Empty( result);
} }
[Fact] [Fact]
public void Should_slugify_value_with_single_char() public async Task ExecuteAndTransformAsync_should_timeout_when_replace_never_called()
{ {
var content = var content = new NamedContentData();
new NamedContentData() var context = new ScriptContext { Data = content, Operation = "MyOperation" };
.AddField("title",
new ContentFieldData()
.AddValue("iv", "4 Häuser"));
var expected =
new NamedContentData()
.AddField("title",
new ContentFieldData()
.AddValue("iv", "4 Häuser"))
.AddField("slug",
new ContentFieldData()
.AddValue("iv", "4-hauser"));
var context = new ScriptContext { Data = content };
const string script = @" const string script = @"
async = true;
var data = ctx.data; var data = ctx.data;
data.slug = { iv: slugify(data.title.iv, true) }; getJSON('http://squidex.io', function(result) {
data.operation = { iv: result.key };
});
replace(data);
"; ";
var result = sut.Transform(context, script); await Assert.ThrowsAnyAsync<OperationCanceledException>(() => sut.ExecuteAndTransformAsync(context, script));
Assert.Equal(expected, result);
} }
[Fact] [Fact]
public void Should_transform_content_and_return_with_execute_transform() public async Task ExecuteAndTransformAsync_should_transform_content_and_return_with_execute_transform()
{ {
var content = var content =
new NamedContentData() new NamedContentData()
@ -320,13 +318,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(data); replace(data);
"; ";
var result = sut.ExecuteAndTransform(context, script); var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }
[Fact] [Fact]
public void Should_transform_content_with_old_content() public async Task ExecuteAndTransformAsync_should_transform_content_with_old_content()
{ {
var content = var content =
new NamedContentData() new NamedContentData()
@ -359,155 +357,77 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(ctx.data); replace(ctx.data);
"; ";
var result = sut.ExecuteAndTransform(context, script); var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }
[Fact] [Fact]
public void Should_evaluate_to_true_when_expression_match() public void Evaluate_should_return_true_when_expression_match()
{ {
const string script = @" const string script = @"
value.i == 2 value.i == 2
"; ";
var result = sut.Evaluate("value", new { i = 2 }, script); var context = new ScriptContext
{
["value"] = new { i = 2 }
};
var result = sut.Evaluate(context, script);
Assert.True(result); Assert.True(result);
} }
[Fact] [Fact]
public void Should_evaluate_to_true_when_status_match() public void Evaluate_should_return_true_when_status_match()
{ {
const string script = @" const string script = @"
value.status == 'Published' value.status == 'Published'
"; ";
var result = sut.Evaluate("value", new { status = Status.Published }, script); var context = new ScriptContext
{
["value"] = new { status = Status.Published }
};
var result = sut.Evaluate(context, script);
Assert.True(result); Assert.True(result);
} }
[Fact] [Fact]
public void Should_evaluate_to_false_when_expression_match() public void Evaluate_should_return_false_when_expression_match()
{ {
const string script = @" const string script = @"
value.i == 3 value.i == 3
"; ";
var result = sut.Evaluate("value", new { i = 2 }, script); var context = new ScriptContext
Assert.False(result);
}
[Fact]
public void Should_evaluate_to_false_when_script_is_invalid()
{
const string script = @"
function();
";
var result = sut.Evaluate("value", new { i = 2 }, script);
Assert.False(result);
}
[Fact]
public async Task Should_make_json_request()
{
var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)
{ {
Content = new StringContent("{ \"key\": 42 }") ["value"] = new { i = 2 }
}; };
var httpHandler = new MockupHander(httpResponse); var result = sut.Evaluate(context, script);
A.CallTo(() => httpClientFactory.CreateClient(A<string>._))
.Returns(new HttpClient(httpHandler));
const string script = @"
getJSON('http://squidex.io', function(result) {
complete(result);
});
";
var result = await sut.GetAsync(new ScriptContext(), script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/");
var expectedResult = JsonValue.Object().Add("key", 42);
Assert.Equal(expectedResult, result); Assert.False(result);
} }
[Fact] [Fact]
public async Task Should_make_json_request_with_headers() public void Evaluate_should_return_false_when_script_is_invalid()
{ {
var httpResponse = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{ \"key\": 42 }")
};
var httpHandler = new MockupHander(httpResponse);
A.CallTo(() => httpClientFactory.CreateClient(A<string>._))
.Returns(new HttpClient(httpHandler));
const string script = @" const string script = @"
var headers = { function();
'X-Header1': 1,
'X-Header2': '2'
};
getJSON('http://squidex.io', function(result) {
complete(result);
}, headers);
"; ";
var result = await sut.GetAsync(new ScriptContext(), script); var context = new ScriptContext
httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/");
httpHandler.ShouldBeHeader("X-Header1", "1");
httpHandler.ShouldBeHeader("X-Header2", "2");
var expectedResult = JsonValue.Object().Add("key", 42);
Assert.Equal(expectedResult, result);
}
private sealed class MockupHander : HttpMessageHandler
{
private readonly HttpResponseMessage response;
private HttpRequestMessage madeRequest;
public void ShouldBeMethod(HttpMethod method)
{
Assert.Equal(method, madeRequest.Method);
}
public void ShouldBeUrl(string url)
{
Assert.Equal(url, madeRequest.RequestUri.ToString());
}
public void ShouldBeHeader(string key, string value)
{ {
Assert.Equal(value, madeRequest.Headers.GetValues(key).FirstOrDefault()); ["value"] = new { i = 2 }
} };
public MockupHander(HttpResponseMessage response) var result = sut.Evaluate(context, script);
{
this.response = response;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) Assert.False(result);
{
madeRequest = request;
return Task.FromResult(response);
}
} }
} }
} }

50
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs

@ -0,0 +1,50 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.Scripting
{
internal sealed class MockupHttpHandler : HttpMessageHandler
{
private readonly HttpResponseMessage response;
private HttpRequestMessage madeRequest;
public void ShouldBeMethod(HttpMethod method)
{
Assert.Equal(method, madeRequest.Method);
}
public void ShouldBeUrl(string url)
{
Assert.Equal(url, madeRequest.RequestUri.ToString());
}
public void ShouldBeHeader(string key, string value)
{
Assert.Equal(value, madeRequest.Headers.GetValues(key).FirstOrDefault());
}
public MockupHttpHandler(HttpResponseMessage response)
{
this.response = response;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
await Task.Delay(1000);
madeRequest = request;
return response;
}
}
}

1
backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FakeItEasy" Version="6.0.0" /> <PackageReference Include="FakeItEasy" Version="6.0.0" />
<PackageReference Include="FluentAssertions" Version="5.10.2" /> <PackageReference Include="FluentAssertions" Version="5.10.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.2" /> <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs

@ -32,10 +32,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
public AssetChangedTriggerHandlerTests() public AssetChangedTriggerHandlerTests()
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "true")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true"))
.Returns(true); .Returns(true);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "false")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false); .Returns(false);
sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader); sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader);
@ -149,12 +149,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
else else
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

24
backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs

@ -10,6 +10,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
@ -31,10 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Comments
public CommentTriggerHandlerTests() public CommentTriggerHandlerTests()
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "true")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true"))
.Returns(true); .Returns(true);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "false")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false); .Returns(false);
sut = new CommentTriggerHandler(scriptEngine, userResolver); sut = new CommentTriggerHandler(scriptEngine, userResolver);
@ -265,27 +267,35 @@ namespace Squidex.Domain.Apps.Entities.Comments
private void TestForRealCondition(string condition, Action<IRuleTriggerHandler, CommentTrigger> action) private void TestForRealCondition(string condition, Action<IRuleTriggerHandler, CommentTrigger> action)
{ {
var trigger = new CommentTrigger { Condition = condition }; var trigger = new CommentTrigger
{
Condition = condition
};
var memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
var handler = new CommentTriggerHandler(new JintScriptEngine(null), userResolver); var handler = new CommentTriggerHandler(new JintScriptEngine(memoryCache), userResolver);
action(handler, trigger); action(handler, trigger);
} }
private void TestForCondition(string condition, Action<CommentTrigger> action) private void TestForCondition(string condition, Action<CommentTrigger> action)
{ {
var trigger = new CommentTrigger { Condition = condition }; var trigger = new CommentTrigger
{
Condition = condition
};
action(trigger); action(trigger);
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
else else
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs

@ -39,10 +39,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
public ContentChangedTriggerHandlerTests() public ContentChangedTriggerHandlerTests()
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "true")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true"))
.Returns(true); .Returns(true);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "false")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false); .Returns(false);
sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader); sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader);
@ -249,12 +249,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, A<string>._)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, A<string>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
else else
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

40
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -101,8 +101,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)) A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId))
.Returns((app, schema)); .Returns((app, schema));
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, A<string>._)) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._))
.ReturnsLazily(x => x.GetArgument<ScriptContext>(0)!.Data!); .ReturnsLazily(x => Task.FromResult(x.GetArgument<ScriptContext>(0)!.Data!));
patched = patch.MergeInto(data); patched = patch.MergeInto(data);
@ -136,9 +136,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft }) CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft })
); );
A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "<create-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(data, null, Status.Draft), "<create-script>"))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>._, "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -159,9 +159,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })
); );
A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(data, null, Status.Draft), "<create-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(data, null, Status.Draft), "<create-script>"))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published), "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Published), "<change-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentUpdated { Data = otherData }) CreateContentEvent(new ContentUpdated { Data = otherData })
); );
A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Draft), "<update-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(otherData, data, Status.Draft), "<update-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -215,7 +215,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentUpdated { Data = otherData }) CreateContentEvent(new ContentUpdated { Data = otherData })
); );
A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(otherData, data, Status.Draft), "<update-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(otherData, data, Status.Draft), "<update-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -232,7 +232,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(LastEvents); Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, "<update-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, "<update-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -264,7 +264,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentUpdated { Data = patched }) CreateContentEvent(new ContentUpdated { Data = patched })
); );
A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Draft), "<update-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(patched, data, Status.Draft), "<update-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -288,7 +288,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentUpdated { Data = patched }) CreateContentEvent(new ContentUpdated { Data = patched })
); );
A.CallTo(() => scriptEngine.ExecuteAndTransform(ScriptContext(patched, data, Status.Draft), "<update-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(ScriptContext(patched, data, Status.Draft), "<update-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -305,7 +305,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(LastEvents); Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, "<update-script>")) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, "<update-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -327,7 +327,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })
); );
A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published, Status.Draft), "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Published, Status.Draft), "<change-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -349,7 +349,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
); );
A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Archived, Status.Draft), "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "<change-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -372,7 +372,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished })
); );
A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft, Status.Published), "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Draft, Status.Published), "<change-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -396,7 +396,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Change, Status = Status.Archived }) CreateContentEvent(new ContentStatusChanged { Change = StatusChange.Change, Status = Status.Archived })
); );
A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Archived, Status.Draft), "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Archived, Status.Draft), "<change-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -423,7 +423,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime })
); );
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>._, "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -451,7 +451,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
); );
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>._, "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }
@ -479,7 +479,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentSchedulingCancelled()) CreateContentEvent(new ContentSchedulingCancelled())
); );
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>._, "<change-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>"))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -501,7 +501,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentDeleted()) CreateContentEvent(new ContentDeleted())
); );
A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Draft), "<delete-script>")) A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Draft), "<delete-script>"))
.MustHaveHappened(); .MustHaveHappened();
} }

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs

@ -10,6 +10,8 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
@ -90,7 +92,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => app.Workflows) A.CallTo(() => app.Workflows)
.Returns(workflows); .Returns(workflows);
sut = new DynamicContentWorkflow(new JintScriptEngine(null), appProvider); var memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
sut = new DynamicContentWorkflow(new JintScriptEngine(memoryCache), appProvider);
} }
[Fact] [Fact]

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ScriptContentTests.cs

@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); await sut.EnrichAsync(ctx, new[] { content }, schemaProvider);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, A<string>._)) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); await sut.EnrichAsync(ctx, new[] { content }, schemaProvider);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, A<string>._)) A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -93,14 +93,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var content = new ContentEntity { SchemaId = schemaWithScriptId, Data = oldData }; var content = new ContentEntity { SchemaId = schemaWithScriptId, Data = oldData };
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>._, "my-query")) A.CallTo(() => scriptEngine.TransformAsync(A<ScriptContext>._, "my-query"))
.Returns(new NamedContentData()); .Returns(new NamedContentData());
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider); await sut.EnrichAsync(ctx, new[] { content }, schemaProvider);
Assert.NotSame(oldData, content.Data); Assert.NotSame(oldData, content.Data);
A.CallTo(() => scriptEngine.Transform( A.CallTo(() => scriptEngine.TransformAsync(
A<ScriptContext>.That.Matches(x => A<ScriptContext>.That.Matches(x =>
ReferenceEquals(x.User, ctx.User) && ReferenceEquals(x.User, ctx.User) &&
ReferenceEquals(x.Data, oldData) && ReferenceEquals(x.Data, oldData) &&

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaChangedTriggerHandlerTests.cs

@ -31,10 +31,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public SchemaChangedTriggerHandlerTests() public SchemaChangedTriggerHandlerTests()
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "true")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true"))
.Returns(true); .Returns(true);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "false")) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false); .Returns(false);
sut = new SchemaChangedTriggerHandler(scriptEngine); sut = new SchemaChangedTriggerHandler(scriptEngine);
@ -136,12 +136,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
else else
{ {
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition)) A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustHaveHappened(); .MustHaveHappened();
} }
} }

Loading…
Cancel
Save