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 customFunctions = new Dictionary<string, Func<string>>
var context = new ScriptContext
{
["contentUrl"] = () => ContentUrl(@event),
["contentAction"] = () => ContentAction(@event)
["event"] = @event
};
return scriptEngine.Interpolate("event", @event, script, customFunctions);
return scriptEngine.Interpolate(context, script);
}
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:
result = JintUser.Create(engine, principal);
return true;
case Guid guid:
result = guid.ToString();
return true;
case Instant instant:
result = JsValue.FromObject(engine, instant.ToDateTimeUtc());
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.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json.Objects;
@ -15,16 +13,16 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
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);
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.Collections.Generic;
using System.Net.Http;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Esprima;
using Jint;
using Jint.Native;
using Jint.Runtime;
using Jint.Runtime.Interop;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Domain.Apps.Core.Scripting.Internal;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
@ -25,74 +26,73 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
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 JintScriptEngine(IHttpClientFactory? httpClientFactory = null)
{
this.httpClientFactory = httpClientFactory;
}
public TimeSpan ExecutionTimeout { get; set; } = TimeSpan.FromMilliseconds(4000);
public void Execute(ScriptContext context, string script)
public JintScriptEngine(IMemoryCache memoryCache, IEnumerable<IScriptExtension>? extensions = null)
{
Guard.NotNull(context);
if (!string.IsNullOrWhiteSpace(script))
{
var engine =
CreateScriptEngine()
.AddContext(context)
.AddDisallow()
.AddReject();
parser = new Parser(memoryCache);
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.NotNullOrEmpty(script);
var result = context.Data!;
if (!string.IsNullOrWhiteSpace(script))
using (var cts = new CancellationTokenSource(ExecutionTimeout))
{
var engine =
CreateScriptEngine()
.AddContext(context)
.AddDisallow()
.AddReject();
var tcs = new TaskCompletionSource<bool>();
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.NotNullOrEmpty(script);
var result = context.Data!;
if (!string.IsNullOrWhiteSpace(script))
using (var cts = new CancellationTokenSource(ExecutionTimeout))
{
try
var tcs = new TaskCompletionSource<NamedContentData>();
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{
var engine =
CreateScriptEngine()
.AddContext(context);
CreateEngine(context, true, cts.Token, tcs.TrySetException, true)
.AddDisallow()
.AddReject();
engine.SetValue("complete", new Action<JsValue?>(value =>
{
tcs.TrySetResult(context.Data!);
}));
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)
{
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);
}
catch (Exception)
{
result = context.Data!;
}
}
Execute(engine, script);
return result;
}
if (engine.GetValue("async") != true)
{
tcs.TrySetResult(context.Data!);
}
private static void Execute(Engine engine, string script)
{
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));
return await tcs.Task;
}
}
}
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
{
var result =
CreateScriptEngine(NullPropagation.Instance)
.SetValue(name, context)
.Execute(script)
.GetCompletionValue()
.ToObject();
var engine = CreateEngine(context, false);
Execute(engine, script);
var converted = Equals(engine.GetCompletionValue().ToObject(), true);
return (bool)result;
return converted;
}
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
{
var result =
CreateScriptEngine(NullPropagation.Instance)
.AddFormatters(customFormatters)
.SetValue(name, context)
.Execute(script)
.GetCompletionValue()
.ToObject();
var engine = CreateEngine(context, false);
Execute(engine, script);
var converted = result.ToString();
var converted = engine.GetCompletionValue().ToObject()?.ToString() ?? "null";
return converted == "undefined" ? "null" : converted;
}
@ -195,36 +220,89 @@ namespace Squidex.Domain.Apps.Core.Scripting
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>();
using (cts.Token.Register(() =>
{
tcs.SetCanceled();
tcs.TrySetCanceled();
}))
{
var engine =
CreateScriptEngine()
.AddContext(context);
if (httpClientFactory != null)
{
var http = new JintHttp(httpClientFactory, cts.Token, tcs.SetException);
http.Add(engine);
}
var engine = CreateEngine(context, true, cts.Token, ex => tcs.TrySetException(ex), true);
engine.SetValue("complete", new Action<JsValue?>(value =>
{
tcs.SetResult(JsonMapper.Map(value));
tcs.TrySetResult(JsonMapper.Map(value));
}));
engine.Execute(script);
if (engine.GetValue("async") != true)
{
tcs.TrySetResult(JsonMapper.Map(engine.GetCompletionValue()));
}
}
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.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using Jint.Native;
using Jint.Native.Object;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
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)
{
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)
{
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)
{
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);
}
public Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptContext context)
public async Task<NamedContentData> ExecuteScriptAndTransformAsync(Func<SchemaScripts, string> script, ScriptContext 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);
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)

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))
{
return scriptEngine.Evaluate("data", data, condition.Expression);
var context = new ScriptContext
{
["data"] = data
};
return scriptEngine.Evaluate(context, condition.Expression);
}
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 scriptContext = new ScriptContext { User = context.User };
foreach (var content in group)
{
scriptContext.Data = content.Data;
scriptContext.ContentId = content.Id;
content.Data = scriptEngine.Transform(scriptContext, script);
}
await Task.WhenAll(group.Select(x => TransformAsync(context, script, x)));
}
}
}
}
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)
{
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)
{
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.UI;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Tags;
@ -58,6 +59,15 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<JintScriptEngine>()
.AsOptional<IScriptEngine>();
services.AddSingletonAs<DateTimeScriptExtension>()
.As<IScriptExtension>();
services.AddSingletonAs<StringScriptExtension>()
.As<IScriptExtension>();
services.AddSingletonAs<HttpScriptExtension>()
.As<IScriptExtension>();
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.DependencyInjection;
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.Comments;
using Squidex.Domain.Apps.Entities.Contents;
@ -64,6 +66,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<RuleRegistry>()
.As<ITypeProvider>().AsSelf();
services.AddSingletonAs<EventScriptExtension>()
.As<IScriptExtension>();
services.AddSingletonAs<RuleEventFormatter>()
.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.Security.Claims;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
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.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Shared.Identity;
@ -46,7 +50,16 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId))
.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]
@ -187,6 +200,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory]
[InlineData("$CONTENT_STATUS")]
[InlineData("Script(contentAction())")]
[InlineData("Script(`${event.status}`)")]
public void Should_format_content_status_when_found(string script)
{
@ -200,7 +214,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory]
[InlineData("$CONTENT_ACTION")]
[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();
@ -224,7 +238,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory]
[InlineData("$CONTENT_ACTION")]
[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();

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.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Validation;
using Xunit;
@ -30,135 +29,165 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
public JintScriptEngineTests()
{
sut = new JintScriptEngine(httpClientFactory)
var extensions = new IScriptExtension[]
{
Timeout = TimeSpan.FromSeconds(1)
new DateTimeScriptExtension(),
new HttpScriptExtension(httpClientFactory),
new StringScriptExtension()
};
}
[Fact]
public void Should_throw_validation_exception_when_calling_reject()
{
const string script = @"
reject()
";
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));
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]
public void Should_throw_validation_exception_when_calling_reject_with_message()
public async Task ExecuteAsync_should_catch_script_syntax_errors()
{
const string script = @"
reject('Not valid')
invalid()
";
var ex = Assert.Throws<ValidationException>(() => sut.Execute(new ScriptContext(), script));
Assert.Equal("Not valid", ex.Errors.Single().Message);
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script));
}
[Fact]
public void Should_throw_security_exception_when_calling_reject()
public async Task ExecuteAsync_should_catch_script_runtime_errors()
{
const string script = @"
disallow()
throw 'Error';
";
var ex = Assert.Throws<DomainForbiddenException>(() => sut.Execute(new ScriptContext(), script));
Assert.Equal("Not allowed", ex.Message);
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptContext(), script));
}
[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 = @"
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]
public void Should_catch_script_syntax_errors()
public async Task TransformAsync_should_transform_content()
{
const string script = @"
invalid()
";
var content =
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 = @"
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]
public void Should_catch_script_runtime_errors_on_execute_and_transform()
public async Task ExecuteAndTransformAsync_should_catch_javascript_error()
{
const string script = @"
throw 'Error';
";
Assert.Throws<ValidationException>(() => sut.ExecuteAndTransform(new ScriptContext(), script));
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAndTransformAsync(new ScriptContext(), script));
}
[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 context = new ScriptContext { Data = content };
const string script = @"
x => x
invalid();
";
var result = sut.Transform(context, script);
Assert.Same(content, result);
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAndTransformAsync(context, script));
}
[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 context = new ScriptContext { Data = content };
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]
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 context = new ScriptContext { Data = content };
const string script = @"
async = true;
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]
public void Should_fetch_operation_name()
public async Task ExecuteAndTransformAsync_should_transform_object()
{
var content = new NamedContentData();
@ -178,117 +207,86 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(data);
";
var result = sut.ExecuteAndTransform(context, script);
var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result);
}
[Fact]
public void Should_transform_content_and_return_with_transform()
public async Task ExecuteAndTransformAsync_should_transform_object_async()
{
var content =
new NamedContentData()
.AddField("number0",
new ContentFieldData()
.AddValue("iv", 1.0))
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 1.0));
var content = new NamedContentData();
var expected =
new NamedContentData()
.AddField("number1",
new ContentFieldData()
.AddValue("iv", 2.0))
.AddField("number2",
.AddField("operation",
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 = @"
async = true;
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;
data.number2 = { 'iv': 10 };
replace(data);
});
replace(data);
";
var result = sut.Transform(context, script);
var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result);
}
[Fact]
public void Should_slugify_value()
public async Task ExecuteAndTransformAsync_should_ignore_transformation_when_async_not_set()
{
var content =
new NamedContentData()
.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 };
var content = new NamedContentData();
var context = new ScriptContext { Data = content, Operation = "MyOperation" };
const string script = @"
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]
public void Should_slugify_value_with_single_char()
public async Task ExecuteAndTransformAsync_should_timeout_when_replace_never_called()
{
var content =
new NamedContentData()
.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 };
var content = new NamedContentData();
var context = new ScriptContext { Data = content, Operation = "MyOperation" };
const string script = @"
async = true;
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);
Assert.Equal(expected, result);
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => sut.ExecuteAndTransformAsync(context, script));
}
[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 =
new NamedContentData()
@ -320,13 +318,13 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(data);
";
var result = sut.ExecuteAndTransform(context, script);
var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result);
}
[Fact]
public void Should_transform_content_with_old_content()
public async Task ExecuteAndTransformAsync_should_transform_content_with_old_content()
{
var content =
new NamedContentData()
@ -359,155 +357,77 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
replace(ctx.data);
";
var result = sut.ExecuteAndTransform(context, script);
var result = await sut.ExecuteAndTransformAsync(context, script);
Assert.Equal(expected, result);
}
[Fact]
public void Should_evaluate_to_true_when_expression_match()
public void Evaluate_should_return_true_when_expression_match()
{
const string script = @"
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);
}
[Fact]
public void Should_evaluate_to_true_when_status_match()
public void Evaluate_should_return_true_when_status_match()
{
const string script = @"
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);
}
[Fact]
public void Should_evaluate_to_false_when_expression_match()
public void Evaluate_should_return_false_when_expression_match()
{
const string script = @"
value.i == 3
";
var result = sut.Evaluate("value", new { i = 2 }, script);
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)
var context = new ScriptContext
{
Content = new StringContent("{ \"key\": 42 }")
["value"] = new { i = 2 }
};
var httpHandler = new MockupHander(httpResponse);
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);
var result = sut.Evaluate(context, script);
Assert.Equal(expectedResult, result);
Assert.False(result);
}
[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 = @"
var headers = {
'X-Header1': 1,
'X-Header2': '2'
};
getJSON('http://squidex.io', function(result) {
complete(result);
}, headers);
function();
";
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 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)
var context = new ScriptContext
{
Assert.Equal(value, madeRequest.Headers.GetValues(key).FirstOrDefault());
}
["value"] = new { i = 2 }
};
public MockupHander(HttpResponseMessage response)
{
this.response = response;
}
var result = sut.Evaluate(context, script);
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
madeRequest = request;
return Task.FromResult(response);
}
Assert.False(result);
}
}
}

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>
<PackageReference Include="FakeItEasy" Version="6.0.0" />
<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.NET.Test.Sdk" Version="16.5.0" />
<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()
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "true"))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true"))
.Returns(true);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "false"))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false);
sut = new AssetChangedTriggerHandler(scriptEngine, assetLoader);
@ -149,12 +149,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
if (string.IsNullOrWhiteSpace(condition))
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustNotHaveHappened();
}
else
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.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.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers;
@ -31,10 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Comments
public CommentTriggerHandlerTests()
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "true"))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true"))
.Returns(true);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "false"))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false);
sut = new CommentTriggerHandler(scriptEngine, userResolver);
@ -265,27 +267,35 @@ namespace Squidex.Domain.Apps.Entities.Comments
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);
}
private void TestForCondition(string condition, Action<CommentTrigger> action)
{
var trigger = new CommentTrigger { Condition = condition };
var trigger = new CommentTrigger
{
Condition = condition
};
action(trigger);
if (string.IsNullOrWhiteSpace(condition))
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustNotHaveHappened();
}
else
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustHaveHappened();
}
}

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

@ -39,10 +39,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
public ContentChangedTriggerHandlerTests()
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "true"))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true"))
.Returns(true);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "false"))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false);
sut = new ContentChangedTriggerHandler(scriptEngine, contentLoader);
@ -249,12 +249,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (string.IsNullOrWhiteSpace(condition))
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, A<string>._))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, A<string>._))
.MustNotHaveHappened();
}
else
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.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))
.Returns((app, schema));
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, A<string>._))
.ReturnsLazily(x => x.GetArgument<ScriptContext>(0)!.Data!);
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._))
.ReturnsLazily(x => Task.FromResult(x.GetArgument<ScriptContext>(0)!.Data!));
patched = patch.MergeInto(data);
@ -136,9 +136,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>._, "<change-script>"))
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>"))
.MustNotHaveHappened();
}
@ -159,9 +159,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
A.CallTo(() => scriptEngine.Execute(ScriptContext(data, null, Status.Published), "<change-script>"))
A.CallTo(() => scriptEngine.ExecuteAsync(ScriptContext(data, null, Status.Published), "<change-script>"))
.MustHaveHappened();
}
@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -215,7 +215,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -232,7 +232,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, "<update-script>"))
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, "<update-script>"))
.MustNotHaveHappened();
}
@ -264,7 +264,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -288,7 +288,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -305,7 +305,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(LastEvents);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, "<update-script>"))
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, "<update-script>"))
.MustNotHaveHappened();
}
@ -327,7 +327,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -349,7 +349,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -372,7 +372,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -396,7 +396,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -423,7 +423,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}
@ -451,7 +451,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentStatusChanged { Status = Status.Archived })
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>._, "<change-script>"))
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>"))
.MustHaveHappened();
}
@ -479,7 +479,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
CreateContentEvent(new ContentSchedulingCancelled())
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>._, "<change-script>"))
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptContext>._, "<change-script>"))
.MustNotHaveHappened();
}
@ -501,7 +501,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
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();
}

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

@ -10,6 +10,8 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
@ -90,7 +92,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => app.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]

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);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, A<string>._))
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._))
.MustNotHaveHappened();
}
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>._, A<string>._))
A.CallTo(() => scriptEngine.ExecuteAndTransformAsync(A<ScriptContext>._, A<string>._))
.MustNotHaveHappened();
}
@ -93,14 +93,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
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());
await sut.EnrichAsync(ctx, new[] { content }, schemaProvider);
Assert.NotSame(oldData, content.Data);
A.CallTo(() => scriptEngine.Transform(
A.CallTo(() => scriptEngine.TransformAsync(
A<ScriptContext>.That.Matches(x =>
ReferenceEquals(x.User, ctx.User) &&
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()
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "true"))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "true"))
.Returns(true);
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, "false"))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, "false"))
.Returns(false);
sut = new SchemaChangedTriggerHandler(scriptEngine);
@ -136,12 +136,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas
if (string.IsNullOrWhiteSpace(condition))
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustNotHaveHappened();
}
else
{
A.CallTo(() => scriptEngine.Evaluate("event", A<object>._, condition))
A.CallTo(() => scriptEngine.Evaluate(A<ScriptContext>._, condition))
.MustHaveHappened();
}
}

Loading…
Cancel
Save