Browse Source

Fix/surrogate keys (#637)

* Fix surrogate keys.

* Fallback handling.

* Script and template extensions for references and assets.

* Bugfix.

* Lazy references.
pull/641/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
ba9e2e2bdc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs
  2. 45
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ExecutionContext.cs
  3. 21
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs
  4. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs
  5. 28
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  6. 37
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs
  7. 26
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  9. 100
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs
  10. 123
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs
  11. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  12. 140
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  13. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  14. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs
  15. 39
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  16. 57
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs
  17. 125
      backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs
  18. 32
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  19. 9
      backend/src/Squidex/Config/Domain/RuleServices.cs
  20. 63
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  21. 106
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs
  22. 139
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs
  23. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs
  24. 174
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs
  25. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  26. 196
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs
  27. 41
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs
  28. 150
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs
  29. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs
  30. 1
      backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs

2
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
this.urlGenerator = urlGenerator;
}
public void Extend(ExecutionContext context, bool async)
public void Extend(ExecutionContext context)
{
context.Engine.SetValue("contentAction", new EventDelegate(() =>
{

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

@ -15,26 +15,19 @@ using Squidex.Text;
namespace Squidex.Domain.Apps.Core.Scripting
{
public delegate bool ExceptionHandler(Exception exception);
public sealed class ExecutionContext : Dictionary<string, object>
public sealed class ExecutionContext : ScriptContext
{
private readonly ExceptionHandler? exceptionHandler;
private Func<Exception, bool>? completion;
public Engine Engine { get; }
public CancellationToken CancellationToken { get; }
public CancellationToken CancellationToken { get; private set; }
public bool IsAsync { get; private set; }
internal ExecutionContext(Engine engine, CancellationToken cancellationToken, ExceptionHandler? exceptionHandler = null)
: base(StringComparer.OrdinalIgnoreCase)
internal ExecutionContext(Engine engine)
{
Engine = engine;
CancellationToken = cancellationToken;
this.exceptionHandler = exceptionHandler;
}
public void MarkAsync()
@ -44,10 +37,34 @@ namespace Squidex.Domain.Apps.Core.Scripting
public void Fail(Exception exception)
{
exceptionHandler?.Invoke(exception);
completion?.Invoke(exception);
}
public ExecutionContext ExtendAsync(IEnumerable<IJintExtension> extensions, Func<Exception, bool> completion, CancellationToken ct)
{
CancellationToken = ct;
this.completion = completion;
foreach (var extension in extensions)
{
extension.ExtendAsync(this);
}
public void AddVariables(ScriptVars vars, ScriptOptions options)
return this;
}
public ExecutionContext Extend(IEnumerable<IJintExtension> extensions)
{
foreach (var extension in extensions)
{
extension.Extend(this);
}
return this;
}
public ExecutionContext Extend(ScriptVars vars, ScriptOptions options)
{
var engine = Engine;
@ -86,6 +103,8 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
engine.SetValue("async", true);
return this;
}
}
}

21
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs

@ -29,14 +29,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
this.httpClientFactory = httpClientFactory;
}
public void Extend(ExecutionContext context, bool async)
public void ExtendAsync(ExecutionContext context)
{
if (async)
{
var engine = context.Engine;
var action = new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers));
engine.SetValue("getJSON", new GetJsonDelegate((url, callback, headers) => GetJson(context, url, callback, headers)));
}
context.Engine.SetValue("getJSON", action);
}
private void GetJson(ExecutionContext context, string url, Action<JsValue> callback, JsValue? headers)
@ -46,6 +43,18 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions
private async Task GetJsonAsync(ExecutionContext context, string url, Action<JsValue> callback, JsValue? headers)
{
if (callback == null)
{
context.Fail(new JavaScriptException("Callback cannot be null."));
return;
}
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
context.Fail(new JavaScriptException("URL is not valid."));
return;
}
context.MarkAsync();
try

6
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IJintExtension.cs

@ -15,7 +15,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
{
}
void Extend(ExecutionContext context, bool async)
void Extend(ExecutionContext context)
{
}
void ExtendAsync(ExecutionContext context)
{
}
}

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

@ -52,7 +52,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{
var context = CreateEngine(vars, options, tcs.TrySetException, true, cts.Token);
var context =
CreateEngine(options)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions, tcs.TrySetException, cts.Token);
context.Engine.SetValue("complete", new Action<JsValue?>(value =>
{
@ -82,7 +86,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
using (cts.Token.Register(() => tcs.TrySetCanceled()))
{
var context = CreateEngine(vars, options, tcs.TrySetException, true, cts.Token);
var context =
CreateEngine(options)
.Extend(vars, options)
.Extend(extensions)
.ExtendAsync(extensions, tcs.TrySetException, cts.Token);
context.Engine.SetValue("complete", new Action<JsValue?>(_ =>
{
@ -126,14 +134,17 @@ namespace Squidex.Domain.Apps.Core.Scripting
Guard.NotNull(vars, nameof(vars));
Guard.NotNullOrEmpty(script, nameof(script));
var context = CreateEngine(vars, options);
var context =
CreateEngine(options)
.Extend(vars, options)
.Extend(extensions);
Execute(context.Engine, script);
return JsonMapper.Map(context.Engine.GetCompletionValue());
}
private ExecutionContext CreateEngine(ScriptVars vars, ScriptOptions options, ExceptionHandler? exceptionHandler = null, bool async = false, CancellationToken ct = default)
private ExecutionContext CreateEngine(ScriptOptions options)
{
var engine = new Engine(engineOptions =>
{
@ -158,14 +169,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
extension.Extend(engine);
}
var context = new ExecutionContext(engine, ct, exceptionHandler);
context.AddVariables(vars, options);
foreach (var extension in extensions)
{
extension.Extend(context, async);
}
var context = new ExecutionContext(engine);
return context;
}

37
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting
{
public class ScriptContext : Dictionary<string, object?>
{
public ScriptContext()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public bool TryGetValue<T>(string key, [MaybeNullWhen(false)] out T value)
{
Guard.NotNull(key, nameof(key));
value = default!;
if (TryGetValue(key, out var temp) && temp is T typed)
{
value = typed;
return true;
}
return false;
}
}
}

26
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptVars.cs

@ -6,21 +6,17 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Squidex.Domain.Apps.Core.Scripting
{
public sealed class ScriptVars : Dictionary<string, object?>
{
public ScriptVars()
: base(StringComparer.OrdinalIgnoreCase)
public sealed class ScriptVars : ScriptContext
{
}
public ClaimsPrincipal? User
{
get => GetValue<ClaimsPrincipal?>();
@ -68,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
get => GetValue<ContentData?>();
set
{
SetValue(value, "oldData");
SetValue(value, nameof(OldData));
SetValue(value);
}
}
@ -78,11 +74,23 @@ namespace Squidex.Domain.Apps.Core.Scripting
get => GetValue<Status>();
set
{
SetValue(value, "oldStatus");
SetValue(value, nameof(OldStatus));
SetValue(value);
}
}
[Obsolete("Use dataOld")]
public ContentData? OldData
{
get => GetValue<ContentData?>();
}
[Obsolete("Use statusOld")]
public Status? OldStatus
{
get => GetValue<Status?>();
}
public void SetValue(object? value, [CallerMemberName] string? key = null)
{
if (key != null)

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -169,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
public async Task<IAssetEntity?> FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize)
public async Task<IAssetEntity?> FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{

100
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs

@ -0,0 +1,100 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Fluid;
using Fluid.Ast;
using Fluid.Tags;
using GraphQL.Utilities;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetsFluidExtension : IFluidExtension
{
private readonly IServiceProvider serviceProvider;
private sealed class AssetTag : ArgumentsTag
{
private readonly IServiceProvider serviceProvider;
public AssetTag(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments)
{
if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent)
{
var app = await GetAppAsync(enrichedEvent);
if (app == null)
{
return Completion.Normal;
}
var requestContext =
Context.Admin(app).Clone(b => b
.WithoutTotal());
var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue();
var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
var asset = await assetQuery.FindAsync(requestContext, DomainId.Create(id));
if (asset != null)
{
var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue();
context.SetValue(name, asset);
}
}
return Completion.Normal;
}
private Task<IAppEntity?> GetAppAsync(EnrichedEvent enrichedEvent)
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
return appProvider.GetAppAsync(enrichedEvent.AppId.Id, false);
}
}
public AssetsFluidExtension(IServiceProvider serviceProvider)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.serviceProvider = serviceProvider;
}
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
memberAccessStrategy.Register<IAssetEntity>();
memberAccessStrategy.Register<IAssetInfo>();
memberAccessStrategy.Register<IEntity>();
memberAccessStrategy.Register<IEntityWithCreatedBy>();
memberAccessStrategy.Register<IEntityWithLastModifiedBy>();
memberAccessStrategy.Register<IEntityWithVersion>();
memberAccessStrategy.Register<IEnrichedAssetEntity>();
}
public void RegisterLanguageExtensions(FluidParserFactory factory)
{
factory.RegisterTag("asset", new AssetTag(serviceProvider));
}
}
}

123
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs

@ -0,0 +1,123 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using GraphQL.Utilities;
using Jint.Native;
using Jint.Runtime;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetsJintExtension : IJintExtension
{
private delegate void GetAssetsDelegate(JsValue references, Action<JsValue> callback);
private readonly IServiceProvider serviceProvider;
public AssetsJintExtension(IServiceProvider serviceProvider)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.serviceProvider = serviceProvider;
}
public void ExtendAsync(ExecutionContext context)
{
if (!context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
{
return;
}
if (!context.TryGetValue<ClaimsPrincipal>(nameof(ScriptVars.User), out var user))
{
return;
}
var action = new GetAssetsDelegate((references, callback) => GetReferences(context, appId, user, references, callback));
context.Engine.SetValue("getAsset", action);
context.Engine.SetValue("getAssets", action);
}
private void GetReferences(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
GetReferencesAsync(context, appId, user, references, callback).Forget();
}
private async Task GetReferencesAsync(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback, nameof(callback));
var ids = new List<DomainId>();
if (references.IsString())
{
ids.Add(DomainId.Create(references.ToString()));
}
else if (references.IsArray())
{
foreach (var value in references.AsArray())
{
if (value.IsString())
{
ids.Add(DomainId.Create(value.ToString()));
}
}
}
if (ids.Count == 0)
{
var emptyAssets = Array.Empty<IEnrichedAssetEntity>();
callback(JsValue.FromObject(context.Engine, emptyAssets));
return;
}
context.MarkAsync();
try
{
var app = await GetAppAsync(appId);
var requestContext =
new Context(user, app).Clone(b => b
.WithoutTotal());
var assetQuery = serviceProvider.GetRequiredService<IAssetQueryService>();
var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids));
callback(JsValue.FromObject(context.Engine, assets.ToArray()));
}
catch (Exception ex)
{
context.Fail(ex);
}
}
private async Task<IAppEntity> GetAppAsync(DomainId appId)
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
var app = await appProvider.GetAppAsync(appId);
if (app == null)
{
throw new JavaScriptException("App does not exist.");
}
return app;
}
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -21,6 +21,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize);
Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id);
Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, long version = EtagVersion.Any);
Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug);
Task<IEnrichedAssetEntity?> FindGlobalAsync(Context context, DomainId id);
}
}

140
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -10,95 +10,162 @@ using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
public sealed class AssetQueryService : IAssetQueryService
{
private static readonly IResultList<IEnrichedAssetEntity> EmptyAssets = ResultList.CreateFrom<IEnrichedAssetEntity>(0);
private readonly IAssetEnricher assetEnricher;
private readonly IAssetRepository assetRepository;
private readonly IAssetLoader assetLoader;
private readonly IAssetFolderRepository assetFolderRepository;
private readonly AssetQueryParser queryParser;
public AssetQueryService(
IAssetEnricher assetEnricher,
IAssetRepository assetRepository,
IAssetLoader assetLoader,
IAssetFolderRepository assetFolderRepository,
AssetQueryParser queryParser)
{
Guard.NotNull(assetEnricher, nameof(assetEnricher));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(assetLoader, nameof(assetLoader));
Guard.NotNull(assetFolderRepository, nameof(assetFolderRepository));
Guard.NotNull(queryParser, nameof(queryParser));
this.assetEnricher = assetEnricher;
this.assetRepository = assetRepository;
this.assetLoader = assetLoader;
this.assetFolderRepository = assetFolderRepository;
this.queryParser = queryParser;
}
public async Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(DomainId appId, DomainId id)
{
using (Profiler.TraceMethod<AssetQueryService>())
{
var result = new List<IAssetFolderEntity>();
while (id != DomainId.Empty)
{
var folder = await assetFolderRepository.FindAssetFolderAsync(appId, id);
if (folder == null || result.Any(x => x.Id == folder.Id))
{
result.Clear();
break;
}
result.Insert(0, folder);
id = folder.ParentId;
}
return result;
}
}
public async Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId)
{
using (Profiler.TraceMethod<AssetQueryService>())
{
var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId);
return assetFolders;
}
}
public async Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize)
{
Guard.NotNull(context, nameof(context));
var asset = await assetRepository.FindAssetAsync(context.App.Id, hash, fileName, fileSize);
using (Profiler.TraceMethod<AssetQueryService>())
{
var asset = await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize);
if (asset != null)
if (asset == null)
{
return await assetEnricher.EnrichAsync(asset, context);
return null;
}
return null;
return await TransformAsync(context, asset);
}
}
public async Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id)
public async Task<IEnrichedAssetEntity?> FindBySlugAsync(Context context, string slug)
{
Guard.NotNull(context, nameof(context));
var asset = await assetRepository.FindAssetAsync(context.App.Id, id);
using (Profiler.TraceMethod<AssetQueryService>())
{
var asset = await assetRepository.FindAssetBySlugAsync(context.App.Id, slug);
if (asset != null)
if (asset == null)
{
return await assetEnricher.EnrichAsync(asset, context);
return null;
}
return null;
return await TransformAsync(context, asset);
}
}
public async Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(DomainId appId, DomainId id)
public async Task<IEnrichedAssetEntity?> FindGlobalAsync(Context context, DomainId id)
{
var result = new List<IAssetFolderEntity>();
Guard.NotNull(context, nameof(context));
while (id != DomainId.Empty)
using (Profiler.TraceMethod<AssetQueryService>())
{
var folder = await assetFolderRepository.FindAssetFolderAsync(appId, id);
var asset = await assetRepository.FindAssetAsync(id);
if (folder == null || result.Any(x => x.Id == folder.Id))
if (asset == null)
{
result.Clear();
break;
return null;
}
result.Insert(0, folder);
id = folder.ParentId;
return await TransformAsync(context, asset);
}
}
return result;
public async Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id, long version = EtagVersion.Any)
{
Guard.NotNull(context, nameof(context));
using (Profiler.TraceMethod<AssetQueryService>())
{
IAssetEntity? asset;
if (version > EtagVersion.Empty)
{
asset = await assetLoader.GetAsync(context.App.Id, id, version);
}
else
{
asset = await assetRepository.FindAssetAsync(context.App.Id, id);
}
public async Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId)
if (asset == null)
{
var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId);
return null;
}
return assetFolders;
return await TransformAsync(context, asset);
}
}
public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q q)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(q, nameof(q));
if (q == null)
{
return EmptyAssets;
}
using (Profiler.TraceMethod<AssetQueryService>())
{
q = await queryParser.ParseAsync(context, q);
var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q);
@ -108,9 +175,30 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
assets = assets.SortSet(x => x.Id, q.Ids);
}
var enriched = await assetEnricher.EnrichAsync(assets, context);
return await TransformAsync(context, assets);
}
}
return ResultList.Create(assets.Total, enriched);
private async Task<IResultList<IEnrichedAssetEntity>> TransformAsync(Context context, IResultList<IAssetEntity> assets)
{
var transformed = await TransformCoreAsync(context, assets);
return ResultList.Create(assets.Total, transformed);
}
private async Task<IEnrichedAssetEntity> TransformAsync(Context context, IAssetEntity asset)
{
var transformed = await TransformCoreAsync(context, Enumerable.Repeat(asset, 1));
return transformed[0];
}
private async Task<IReadOnlyList<IEnrichedAssetEntity>> TransformCoreAsync(Context context, IEnumerable<IAssetEntity> assets)
{
using (Profiler.TraceMethod<AssetQueryService>())
{
return await assetEnricher.EnrichAsync(assets, context);
}
}
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -21,12 +21,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
Task<IReadOnlyList<DomainId>> QueryChildIdsAsync(DomainId appId, DomainId parentId);
Task<IAssetEntity?> FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize);
Task<IAssetEntity?> FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize);
Task<IAssetEntity?> FindAssetBySlugAsync(DomainId appId, string slug);
Task<IAssetEntity?> FindAssetAsync(DomainId appId);
Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id);
Task<IAssetEntity?> FindAssetBySlugAsync(DomainId appId, string slug);
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs

@ -24,9 +24,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter
this.grainFactory = grainFactory;
}
public void Extend(ExecutionContext context, bool async)
public void Extend(ExecutionContext context)
{
if (context.TryGetValue("appId", out var temp) && temp is DomainId appId)
if (context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
{
var engine = context.Engine;

39
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -25,52 +25,47 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private readonly IAppProvider appProvider;
private readonly IContentEnricher contentEnricher;
private readonly IContentRepository contentRepository;
private readonly IContentLoader contentVersionLoader;
private readonly IContentLoader contentLoader;
private readonly ContentQueryParser queryParser;
public ContentQueryService(
IAppProvider appProvider,
IContentEnricher contentEnricher,
IContentRepository contentRepository,
IContentLoader contentVersionLoader,
IContentLoader assetLoader,
ContentQueryParser queryParser)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(contentEnricher, nameof(contentEnricher));
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader));
Guard.NotNull(assetLoader, nameof(assetLoader));
Guard.NotNull(queryParser, nameof(queryParser));
this.appProvider = appProvider;
this.contentEnricher = contentEnricher;
this.contentRepository = contentRepository;
this.contentVersionLoader = contentVersionLoader;
this.contentLoader = assetLoader;
this.queryParser = queryParser;
this.queryParser = queryParser;
}
public async Task<IEnrichedContentEntity?> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = -1)
public async Task<IEnrichedContentEntity?> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any)
{
Guard.NotNull(context, nameof(context));
if (id == default)
using (Profiler.TraceMethod<ContentQueryService>())
{
throw new DomainObjectNotFoundException(id.ToString());
}
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
using (Profiler.TraceMethod<ContentQueryService>())
{
IContentEntity? content;
if (version > EtagVersion.Empty)
{
content = await FindByVersionAsync(context, id, version);
content = await contentLoader.GetAsync(context.App.Id, id, version);
}
else
{
content = await FindCoreAsync(context, id, schema);
content = await contentRepository.FindContentAsync(context.App, schema, id, context.Scope());
}
if (content == null || content.SchemaId.Id != schema.Id)
@ -86,6 +81,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
Guard.NotNull(context, nameof(context));
using (Profiler.TraceMethod<ContentQueryService>())
{
if (q == null)
{
return EmptyContents;
@ -98,8 +95,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
q = q with { CreatedBy = context.User.Token() };
}
using (Profiler.TraceMethod<ContentQueryService>())
{
q = await queryParser.ParseAsync(context, q, schema);
var contents = await contentRepository.QueryAsync(context.App, schema, q, context.Scope());
@ -117,6 +112,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
Guard.NotNull(context, nameof(context));
using (Profiler.TraceMethod<ContentQueryService>())
{
if (q == null)
{
return EmptyContents;
@ -129,8 +126,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return EmptyContents;
}
using (Profiler.TraceMethod<ContentQueryService>())
{
q = await queryParser.ParseAsync(context, q);
var contents = await contentRepository.QueryAsync(context.App, schemas, q, context.Scope());
@ -218,15 +213,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
return context.UserPermissions.Allows(permissionId, context.App.Name, schema.SchemaDef.Name);
}
private Task<IContentEntity?> FindCoreAsync(Context context, DomainId id, ISchemaEntity schema)
{
return contentRepository.FindContentAsync(context.App, schema, id, context.Scope());
}
private Task<IContentEntity?> FindByVersionAsync(Context context, DomainId id, long version)
{
return contentVersionLoader.GetAsync(context.App.Id, id, version);
}
}
}

57
backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -13,8 +14,10 @@ using System.Threading.Tasks;
using Fluid;
using Fluid.Ast;
using Fluid.Tags;
using GraphQL.Utilities;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
#pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections
@ -23,75 +26,83 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ReferencesFluidExtension : IFluidExtension
{
private readonly IContentQueryService contentQueryService;
private readonly IAppProvider appProvider;
private readonly IServiceProvider serviceProvider;
private sealed class ReferenceTag : ArgumentsTag
{
private readonly IContentQueryService contentQueryService;
private readonly IAppProvider appProvider;
private readonly IServiceProvider serviceProvider;
public ReferenceTag(IContentQueryService contentQueryService, IAppProvider appProvider)
public ReferenceTag(IServiceProvider serviceProvider)
{
this.contentQueryService = contentQueryService;
this.appProvider = appProvider;
this.serviceProvider = serviceProvider;
}
public override async ValueTask<Completion> WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context, FilterArgument[] arguments)
{
if (arguments.Length == 2 && context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent)
{
var app = await appProvider.GetAppAsync(enrichedEvent.AppId.Id, false);
var app = await GetAppAsync(enrichedEvent);
if (app == null)
{
return Completion.Normal;
}
var appContext = Context.Admin(app).Clone(b => b
var requestContext =
Context.Admin(app).Clone(b => b
.WithoutContentEnrichment()
.WithoutCleanup()
.WithUnpublished());
.WithUnpublished()
.WithoutTotal());
var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue();
var domainId = DomainId.Create(id);
var domainIds = new List<DomainId> { domainId };
var references = await contentQueryService.QueryAsync(appContext, Q.Empty.WithIds(domainIds));
var reference = references.FirstOrDefault();
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds));
var content = contents.FirstOrDefault();
if (reference != null)
if (content != null)
{
var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue();
context.SetValue(name, reference);
context.SetValue(name, content);
}
}
return Completion.Normal;
}
}
public ReferencesFluidExtension(IContentQueryService contentQueryService, IAppProvider appProvider)
private Task<IAppEntity?> GetAppAsync(EnrichedEvent enrichedEvent)
{
Guard.NotNull(contentQueryService, nameof(contentQueryService));
Guard.NotNull(appProvider, nameof(appProvider));
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
this.contentQueryService = contentQueryService;
return appProvider.GetAppAsync(enrichedEvent.AppId.Id, false);
}
}
public ReferencesFluidExtension(IServiceProvider serviceProvider)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.appProvider = appProvider;
this.serviceProvider = serviceProvider;
}
public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy)
{
memberAccessStrategy.Register<IContentEntity>();
memberAccessStrategy.Register<IEntity>();
memberAccessStrategy.Register<IEntityWithCreatedBy>();
memberAccessStrategy.Register<IEntityWithLastModifiedBy>();
memberAccessStrategy.Register<IEntityWithVersion>();
memberAccessStrategy.Register<IEnrichedContentEntity>();
}
public void RegisterLanguageExtensions(FluidParserFactory factory)
{
factory.RegisterTag("reference", new ReferenceTag(contentQueryService, appProvider));
factory.RegisterTag("reference", new ReferenceTag(serviceProvider));
}
}
}

125
backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesJintExtension.cs

@ -0,0 +1,125 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Jint.Native;
using Jint.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ReferencesJintExtension : IJintExtension
{
private delegate void GetReferencesDelegate(JsValue references, Action<JsValue> callback);
private readonly IServiceProvider serviceProvider;
public ReferencesJintExtension(IServiceProvider serviceProvider)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.serviceProvider = serviceProvider;
}
public void ExtendAsync(ExecutionContext context)
{
if (!context.TryGetValue<DomainId>(nameof(ScriptVars.AppId), out var appId))
{
return;
}
if (!context.TryGetValue<ClaimsPrincipal>(nameof(ScriptVars.User), out var user))
{
return;
}
var action = new GetReferencesDelegate((references, callback) => GetReferences(context, appId, user, references, callback));
context.Engine.SetValue("getReference", action);
context.Engine.SetValue("getReferences", action);
}
private void GetReferences(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
GetReferencesAsync(context, appId, user, references, callback).Forget();
}
private async Task GetReferencesAsync(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action<JsValue> callback)
{
Guard.NotNull(callback, nameof(callback));
var ids = new List<DomainId>();
if (references.IsString())
{
ids.Add(DomainId.Create(references.ToString()));
}
else if (references.IsArray())
{
foreach (var value in references.AsArray())
{
if (value.IsString())
{
ids.Add(DomainId.Create(value.ToString()));
}
}
}
if (ids.Count == 0)
{
var emptyContents = Array.Empty<IEnrichedContentEntity>();
callback(JsValue.FromObject(context.Engine, emptyContents));
return;
}
context.MarkAsync();
try
{
var app = await GetAppAsync(appId);
var requestContext =
new Context(user, app).Clone(b => b
.WithoutContentEnrichment()
.WithUnpublished()
.WithoutTotal());
var contentQuery = serviceProvider.GetRequiredService<IContentQueryService>();
var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(ids));
callback(JsValue.FromObject(context.Engine, contents.ToArray()));
}
catch (Exception ex)
{
context.Fail(ex);
}
}
private async Task<IAppEntity> GetAppAsync(DomainId appId)
{
var appProvider = serviceProvider.GetRequiredService<IAppProvider>();
var app = await appProvider.GetAppAsync(appId);
if (app == null)
{
throw new JavaScriptException("App does not exist.");
}
return app;
}
}
}

32
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -16,8 +16,8 @@ using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Assets;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Log;
@ -34,7 +34,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
public sealed class AssetContentController : ApiController
{
private readonly IAssetFileStore assetFileStore;
private readonly IAssetRepository assetRepository;
private readonly IAssetQueryService assetQuery;
private readonly IAssetLoader assetLoader;
private readonly IAssetStore assetStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
@ -42,14 +42,14 @@ namespace Squidex.Areas.Api.Controllers.Assets
public AssetContentController(
ICommandBus commandBus,
IAssetFileStore assetFileStore,
IAssetRepository assetRepository,
IAssetQueryService assetQuery,
IAssetLoader assetLoader,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator)
: base(commandBus)
{
this.assetFileStore = assetFileStore;
this.assetRepository = assetRepository;
this.assetQuery = assetQuery;
this.assetLoader = assetLoader;
this.assetStore = assetStore;
this.assetThumbnailGenerator = assetThumbnailGenerator;
@ -74,14 +74,16 @@ namespace Squidex.Areas.Api.Controllers.Assets
[AllowAnonymous]
public async Task<IActionResult> GetAssetContentBySlug(string app, string idOrSlug, [FromQuery] AssetContentQueryDto queries, string? more = null)
{
var asset = await assetRepository.FindAssetAsync(AppId, DomainId.Create(idOrSlug));
var requestContext = Context.Clone(b => b.WithoutAssetEnrichment());
var asset = await assetQuery.FindAsync(requestContext, DomainId.Create(idOrSlug));
if (asset == null)
{
asset = await assetRepository.FindAssetBySlugAsync(AppId, idOrSlug);
asset = await assetQuery.FindBySlugAsync(requestContext, idOrSlug);
}
return await DeliverAssetAsync(asset, queries);
return await DeliverAssetAsync(requestContext, asset, queries);
}
/// <summary>
@ -102,12 +104,14 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Obsolete("Use overload with app name")]
public async Task<IActionResult> GetAssetContent(DomainId id, [FromQuery] AssetContentQueryDto queries)
{
var asset = await assetRepository.FindAssetAsync(id);
var requestContext = Context.Clone(b => b.WithoutAssetEnrichment());
var asset = await assetQuery.FindGlobalAsync(requestContext, id);
return await DeliverAssetAsync(asset, queries);
return await DeliverAssetAsync(requestContext, asset, queries);
}
private async Task<IActionResult> DeliverAssetAsync(IAssetEntity? asset, AssetContentQueryDto queries)
private async Task<IActionResult> DeliverAssetAsync(Context context, IAssetEntity? asset, AssetContentQueryDto queries)
{
queries ??= new AssetContentQueryDto();
@ -125,8 +129,16 @@ namespace Squidex.Areas.Api.Controllers.Assets
if (asset != null && queries.Version > EtagVersion.Any && asset.Version != queries.Version)
{
if (context.App != null)
{
asset = await assetQuery.FindAsync(context, asset.Id, queries.Version);
}
else
{
// Fallback for old endpoint. Does not set the surrogate key.
asset = await assetLoader.GetAsync(asset.AppId.Id, asset.Id, queries.Version);
}
}
if (asset == null)
{

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

@ -47,9 +47,18 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>();
services.AddSingletonAs<AssetsFluidExtension>()
.As<IFluidExtension>();
services.AddSingletonAs<AssetsJintExtension>()
.As<IJintExtension>();
services.AddSingletonAs<ReferencesFluidExtension>()
.As<IFluidExtension>();
services.AddSingletonAs<ReferencesJintExtension>()
.As<IJintExtension>();
services.AddSingletonAs<ManualTriggerHandler>()
.As<IRuleTriggerHandler>();

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

@ -11,6 +11,7 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FakeItEasy;
using Jint.Runtime;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Scripting;
@ -195,7 +196,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanReject = true
};
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script, options));
var vars = new ScriptVars();
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options));
Assert.NotEmpty(ex.Errors);
}
@ -212,7 +215,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanReject = true
};
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(new ScriptVars(), script, options));
var vars = new ScriptVars();
var ex = await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(vars, script, options));
Assert.Equal("Not valid", ex.Errors.Single().Message);
}
@ -229,7 +234,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanDisallow = true
};
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptVars(), script, options));
var vars = new ScriptVars();
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options));
Assert.Equal("Script has forbidden the operation.", ex.Message);
}
@ -246,25 +253,57 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
CanDisallow = true
};
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(new ScriptVars(), script, options));
var vars = new ScriptVars();
var ex = await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.ExecuteAsync(vars, script, options));
Assert.Equal("Operation not allowed", ex.Message);
}
[Fact]
public async Task Should_throw_exception_when_getJson_url_is_null()
{
const string script = @"
getJSON(null, function(result) {
complete(result);
});
";
var vars = new ScriptVars();
await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script));
}
[Fact]
public async Task Should_throw_exception_when_getJson_callback_is_null()
{
const string script = @"
var url = 'http://squidex.io';
getJSON(url, null);
";
var vars = new ScriptVars();
await Assert.ThrowsAsync<JavaScriptException>(() => sut.ExecuteAsync(vars, script));
}
[Fact]
public async Task Should_make_json_request()
{
var httpHandler = SetupRequest();
const string script = @"
async = true;
var url = 'http://squidex.io';
getJSON('http://squidex.io', function(result) {
getJSON(url, function(result) {
complete(result);
});
";
var result = await sut.ExecuteAsync(new ScriptVars(), script);
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/");
@ -280,19 +319,21 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting
var httpHandler = SetupRequest();
const string script = @"
async = true;
var headers = {
'X-Header1': 1,
'X-Header2': '2'
};
getJSON('http://squidex.io', function(result) {
var url = 'http://squidex.io';
getJSON(url, function(result) {
complete(result);
}, headers);
";
var result = await sut.ExecuteAsync(new ScriptVars(), script);
var vars = new ScriptVars();
var result = await sut.ExecuteAsync(vars, script);
httpHandler.ShouldBeMethod(HttpMethod.Get);
httpHandler.ShouldBeUrl("http://squidex.io/");

106
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs

@ -0,0 +1,106 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetsFluidExtensionTests
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly FluidTemplateEngine sut;
public AssetsFluidExtensionTests()
{
var services =
new ServiceCollection()
.AddSingleton(appProvider)
.AddSingleton(assetQuery)
.BuildServiceProvider();
var extensions = new IFluidExtension[]
{
new AssetsFluidExtension(services)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false))
.Returns(Mocks.App(appId));
sut = new FluidTemplateEngine(extensions);
}
[Fact]
public async Task Should_resolve_assets_in_loop()
{
var assetId1 = DomainId.NewGuid();
var asset1 = CreateAsset(assetId1, 1);
var assetId2 = DomainId.NewGuid();
var asset2 = CreateAsset(assetId2, 2);
var @event = new EnrichedContentEvent
{
Data =
new ContentData()
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(assetId1, assetId2))),
AppId = appId
};
A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId1, EtagVersion.Any))
.Returns(asset1);
A.CallTo(() => assetQuery.FindAsync(A<Context>._, assetId2, EtagVersion.Any))
.Returns(asset2);
var vars = new TemplateVars
{
["event"] = @event
};
var template = @"
{% for id in event.data.assets.iv %}
{% asset 'ref', id %}
Text: {{ ref.fileName }} {{ ref.id }}
{% endfor %}
";
var expected = $@"
Text: file1.jpg {assetId1}
Text: file2.jpg {assetId2}
";
var result = await sut.RenderAsync(template, vars);
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index)
{
return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId };
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Replace(" ", string.Empty);
}
}
}

139
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs

@ -0,0 +1,139 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetsJintExtensionTests : IClassFixture<TranslationsFixture>
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly JintScriptEngine sut;
public AssetsJintExtensionTests()
{
var services =
new ServiceCollection()
.AddSingleton(appProvider)
.AddSingleton(assetQuery)
.BuildServiceProvider();
var extensions = new IJintExtension[]
{
new AssetsJintExtension(services)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false))
.Returns(Mocks.App(appId));
sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions);
}
[Fact]
public async Task Should_resolve_asset()
{
var assetId1 = DomainId.NewGuid();
var asset1 = CreateAsset(assetId1, 1);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(assetId1)));
A.CallTo(() => assetQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId1)))
.Returns(ResultList.CreateFrom(1, asset1));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user };
var script = @"
getAsset(data.assets.iv[0], function (assets) {
var result1 = `Text: ${assets[0].fileName}`;
complete(`${result1}`);
})";
var expected = @"
Text: file1.jpg
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
[Fact]
public async Task Should_resolve_assets()
{
var assetId1 = DomainId.NewGuid();
var asset1 = CreateAsset(assetId1, 1);
var assetId2 = DomainId.NewGuid();
var asset2 = CreateAsset(assetId1, 2);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("assets",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(assetId1, assetId2)));
A.CallTo(() => assetQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A<Q>.That.HasIds(assetId1, assetId2)))
.Returns(ResultList.CreateFrom(2, asset1, asset2));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user };
var script = @"
getAssets(data.assets.iv, function (assets) {
var result1 = `Text: ${assets[0].fileName}`;
var result2 = `Text: ${assets[1].fileName}`;
complete(`${result1}\n${result2}`);
})";
var expected = @"
Text: file1.jpg
Text: file2.jpg
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index)
{
return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId };
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Replace(" ", string.Empty);
}
}
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs

@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
{
var random = _.RandomValue();
var assets = await _.AssetRepository.FindAssetAsync(_.RandomAppId(), random, random, 1024);
var assets = await _.AssetRepository.FindAssetByHashAsync(_.RandomAppId(), random, random, 1024);
Assert.NotNull(assets);
}

174
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs

@ -12,6 +12,7 @@ using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets.Queries
@ -20,6 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>();
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly IAssetLoader assetLoader = A.Fake<IAssetLoader>();
private readonly IAssetFolderRepository assetFolderRepository = A.Fake<IAssetFolderRepository>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly Context requestContext;
@ -30,76 +32,167 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId));
SetupEnricher();
A.CallTo(() => queryParser.ParseAsync(requestContext, A<Q>._))
.ReturnsLazily(c => Task.FromResult(c.GetArgument<Q>(1)!));
sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser);
sut = new AssetQueryService(assetEnricher, assetRepository, assetLoader, assetFolderRepository, queryParser);
}
[Fact]
public async Task Should_find_asset_by_slug_and_enrich_it()
{
var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug"))
.Returns(asset);
var result = await sut.FindBySlugAsync(requestContext, "slug");
AssertAsset(asset, result);
}
[Fact]
public async Task Should_return_null_if_asset_by_slug_cannot_be_found()
{
var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug"))
.Returns(Task.FromResult<IAssetEntity?>(null));
var result = await sut.FindBySlugAsync(requestContext, "slug");
Assert.Null(result);
}
[Fact]
public async Task Should_find_asset_by_id_and_enrich_it()
{
var found = new AssetEntity { Id = DomainId.NewGuid() };
var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id))
.Returns(asset);
var result = await sut.FindAsync(requestContext, asset.Id);
AssertAsset(asset, result);
}
[Fact]
public async Task Should_return_null_if_asset_by_id_cannot_be_found()
{
var asset = CreateAsset(DomainId.NewGuid());
var enriched = new AssetEntity();
A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id))
.Returns(Task.FromResult<IAssetEntity?>(null));
A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, found.Id))
.Returns(found);
var result = await sut.FindAsync(requestContext, asset.Id);
A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext))
.Returns(enriched);
Assert.Null(result);
}
[Fact]
public async Task Should_find_asset_by_id_and_version_and_enrich_it()
{
var asset = CreateAsset(DomainId.NewGuid());
var result = await sut.FindAsync(requestContext, found.Id);
A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2))
.Returns(asset);
Assert.Same(enriched, result);
var result = await sut.FindAsync(requestContext, asset.Id, 2);
AssertAsset(asset, result);
}
[Fact]
public async Task Should_find_assets_by_hash_and_and_enrich_it()
public async Task Should_return_null_if_asset_by_id_and_version_cannot_be_found()
{
var found = new AssetEntity { Id = DomainId.NewGuid() };
var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2))
.Returns(Task.FromResult<IAssetEntity?>(null));
var result = await sut.FindAsync(requestContext, asset.Id, 2);
var enriched = new AssetEntity();
Assert.Null(result);
}
[Fact]
public async Task Should_find_global_asset_by_id_and_enrich_it()
{
var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, "hash", "name", 123))
.Returns(found);
A.CallTo(() => assetRepository.FindAssetAsync(asset.Id))
.Returns(asset);
A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext))
.Returns(enriched);
var result = await sut.FindGlobalAsync(requestContext, asset.Id);
AssertAsset(asset, result);
}
[Fact]
public async Task Should_return_null_if_global_asset_by_id_cannot_be_found()
{
var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetAsync(asset.Id))
.Returns(Task.FromResult<IAssetEntity?>(null));
var result = await sut.FindGlobalAsync(requestContext, asset.Id);
Assert.Null(result);
}
[Fact]
public async Task Should_find_assets_by_hash_and_and_enrich_it()
{
var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123))
.Returns(asset);
var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123);
Assert.Same(enriched, result);
AssertAsset(asset, result);
}
[Fact]
public async Task Should_load_assets_with_query_and_resolve_tags()
public async Task Should_return_null_if_asset_by_hash_cannot_be_found()
{
var found1 = new AssetEntity { Id = DomainId.NewGuid() };
var found2 = new AssetEntity { Id = DomainId.NewGuid() };
var asset = CreateAsset(DomainId.NewGuid());
A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123))
.Returns(Task.FromResult<IAssetEntity?>(null));
var enriched1 = new AssetEntity();
var enriched2 = new AssetEntity();
var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123);
Assert.Null(result);
}
[Fact]
public async Task Should_query_assets_and_enrich_it()
{
var asset1 = CreateAsset(DomainId.NewGuid());
var asset2 = CreateAsset(DomainId.NewGuid());
var parentId = DomainId.NewGuid();
var q = Q.Empty.WithODataQuery("fileName eq 'Name'");
A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, q))
.Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext))
.Returns(new List<IEnrichedAssetEntity> { enriched1, enriched2 });
.Returns(ResultList.CreateFrom(8, asset1, asset2));
var result = await sut.QueryAsync(requestContext, parentId, q);
Assert.Equal(8, result.Total);
Assert.Equal(new[] { enriched1, enriched2 }, result.ToArray());
AssertAsset(asset1, result[0]);
AssertAsset(asset2, result[1]);
}
[Fact]
public async Task Should_load_assets_folders_from_repository()
public async Task Should_query_asset_folders()
{
var parentId = DomainId.NewGuid();
@ -114,7 +207,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
}
[Fact]
public async Task Should_resolve_folder_path_from_root()
public async Task Should_find_asset_folder_with_path()
{
var folderId1 = DomainId.NewGuid();
var folder1 = CreateFolder(folderId1);
@ -205,6 +298,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
Assert.Empty(result);
}
private static void AssertAsset(IAssetEntity source, IEnrichedAssetEntity? result)
{
Assert.NotNull(result);
Assert.NotSame(source, result);
Assert.Equal(source.AssetId, result?.AssetId);
}
private static IAssetFolderEntity CreateFolder(DomainId id, DomainId parentId = default)
{
var assetFolder = A.Fake<IAssetFolderEntity>();
@ -214,5 +314,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
return assetFolder;
}
private static AssetEntity CreateAsset(DomainId id)
{
return new AssetEntity { Id = id };
}
private void SetupEnricher()
{
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>._, A<Context>._))
.ReturnsLazily(x =>
{
var input = x.GetArgument<IEnumerable<IAssetEntity>>(0)!;
return Task.FromResult<IReadOnlyList<IEnrichedAssetEntity>>(input.Select(c => SimpleMapper.Map(c, new AssetEntity())).ToList());
});
}
}
}

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

@ -11,7 +11,6 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;

196
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -31,11 +31,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly IContentLoader contentVersionLoader = A.Fake<IContentLoader>();
private readonly ISchemaEntity schema;
private readonly DomainId contentId = DomainId.NewGuid();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema");
private readonly ContentData contentData = new ContentData();
private readonly ContentData contentTransformed = new ContentData();
private readonly ContentQueryParser queryParser = A.Fake<ContentQueryParser>();
private readonly ContentQueryService sut;
@ -67,66 +65,70 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}
[Fact]
public async Task GetSchemaOrThrowAsync_should_return_schema_from_guid_string()
public async Task Should_get_schema_from_guid_string()
{
var input = schemaId.Id.ToString();
var ctx = CreateContext(isFrontend: false, allowSchema: true);
var requestContext = CreateContext();
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true))
.Returns(schema);
var result = await sut.GetSchemaOrThrowAsync(ctx, input);
var result = await sut.GetSchemaOrThrowAsync(requestContext, input);
Assert.Equal(schema, result);
}
[Fact]
public async Task GetSchemaOrThrowAsync_should_return_schema_from_name()
public async Task Should_get_schema_from_name()
{
var input = schemaId.Name;
var ctx = CreateContext(isFrontend: false, allowSchema: true);
var requestContext = CreateContext();
A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true))
.Returns(schema);
var result = await sut.GetSchemaOrThrowAsync(ctx, input);
var result = await sut.GetSchemaOrThrowAsync(requestContext, input);
Assert.Equal(schema, result);
}
[Fact]
public async Task GetSchemaOrThrowAsync_should_throw_404_if_not_found()
public async Task Should_throw_notfound_exception_if_schema_to_get_not_found()
{
var ctx = CreateContext(isFrontend: false, allowSchema: true);
var requestContext = CreateContext();
A.CallTo(() => appProvider.GetSchemaAsync(A<DomainId>._, A<string>._, true))
.Returns((ISchemaEntity?)null);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name));
}
[Fact]
public async Task FindContentAsync_should_throw_exception_if_user_has_no_permission()
public async Task Should_throw_permission_exception_if_content_to_find_is_restricted()
{
var ctx = CreateContext(isFrontend: false, allowSchema: false);
var requestContext = CreateContext(allowSchema: false);
A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._))
.Returns(CreateContent(contentId));
var content = CreateContent(DomainId.NewGuid());
await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.FindAsync(ctx, schemaId.Name, contentId));
A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A<SearchScope>._))
.Returns(CreateContent(DomainId.NewGuid()));
await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.FindAsync(requestContext, schemaId.Name, content.Id));
}
[Fact]
public async Task FindContentAsync_should_return_null_if_not_found()
public async Task Should_return_null_if_content_by_id_dannot_be_found()
{
var ctx = CreateContext(isFrontend: false, allowSchema: true);
var requestContext = CreateContext();
var content = CreateContent(DomainId.NewGuid());
A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._))
A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A<SearchScope>._))
.Returns<IContentEntity?>(null);
Assert.Null(await sut.FindAsync(ctx, schemaId.Name, contentId));
Assert.Null(await sut.FindAsync(requestContext, schemaId.Name, content.Id));
}
[Theory]
@ -134,45 +136,41 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[InlineData(1, 1, SearchScope.All)]
[InlineData(0, 1, SearchScope.All)]
[InlineData(0, 0, SearchScope.Published)]
public async Task FindContentAsync_should_return_content(int isFrontend, int unpublished, SearchScope scope)
public async Task Should_return_content_by_id(int isFrontend, int unpublished, SearchScope scope)
{
var ctx =
CreateContext(isFrontend: isFrontend == 1, allowSchema: true).Clone(b => b
.WithUnpublished(unpublished == 1));
var requestContext = CreateContext(isFrontend, isUnpublished: unpublished);
var content = CreateContent(contentId);
var content = CreateContent(DomainId.NewGuid());
A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, scope))
A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, scope))
.Returns(content);
var result = await sut.FindAsync(ctx, schemaId.Name, contentId);
var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id);
Assert.Equal(contentTransformed, result!.Data);
Assert.Equal(content.Id, result.Id);
AssertContent(content, result);
}
[Fact]
public async Task FindContentAsync_should_return_content_by_version()
public async Task Should_return_content_by_id_and_version()
{
var ctx = CreateContext(isFrontend: false, allowSchema: true);
var requestContext = CreateContext();
var content = CreateContent(contentId);
var content = CreateContent(DomainId.NewGuid());
A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, contentId, 13))
A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, content.Id, 13))
.Returns(content);
var result = await sut.FindAsync(ctx, schemaId.Name, contentId, 13);
var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13);
Assert.Equal(contentTransformed, result!.Data);
Assert.Equal(content.Id, result.Id);
AssertContent(content, result);
}
[Fact]
public async Task QueryAsync_should_throw_if_user_has_no_permission()
public async Task Should_throw_exception_if_user_has_no_permission_to_query_content()
{
var ctx = CreateContext(isFrontend: false, allowSchema: false);
var requestContext = CreateContext(allowSchema: false);
await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.QueryAsync(ctx, schemaId.Name, Q.Empty));
await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.QueryAsync(requestContext, schemaId.Name, Q.Empty));
}
[Theory]
@ -180,89 +178,95 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[InlineData(1, 1, SearchScope.All)]
[InlineData(0, 1, SearchScope.All)]
[InlineData(0, 0, SearchScope.Published)]
public async Task QueryAsync_should_return_contents(int isFrontend, int unpublished, SearchScope scope)
public async Task Should_query_contents(int isFrontend, int unpublished, SearchScope scope)
{
var ctx =
CreateContext(isFrontend: isFrontend == 1, allowSchema: true).Clone(b => b
.WithUnpublished(unpublished == 1));
var requestContext = CreateContext(isFrontend, isUnpublished: unpublished);
var content = CreateContent(contentId);
var content1 = CreateContent(DomainId.NewGuid());
var content2 = CreateContent(DomainId.NewGuid());
var q = Q.Empty.WithReference(DomainId.NewGuid());
A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, q, scope))
.Returns(ResultList.CreateFrom(5, content));
var result = await sut.QueryAsync(ctx, schemaId.Name, q);
A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, q, scope))
.Returns(ResultList.CreateFrom(5, content1, content2));
Assert.Equal(contentData, result[0].Data);
Assert.Equal(contentId, result[0].Id);
var result = await sut.QueryAsync(requestContext, schemaId.Name, q);
Assert.Equal(5, result.Total);
AssertContent(content1, result[0]);
AssertContent(content2, result[1]);
}
[Fact]
public async Task QueryAll_should_not_return_contents_if_user_has_no_permission()
[Theory]
[InlineData(1, 0, SearchScope.All)]
[InlineData(1, 1, SearchScope.All)]
[InlineData(0, 1, SearchScope.All)]
[InlineData(0, 0, SearchScope.Published)]
public async Task Should_query_contents_by_ids(int isFrontend, int unpublished, SearchScope scope)
{
var ctx = CreateContext(isFrontend: false, allowSchema: false);
var requestContext = CreateContext(isFrontend, isUnpublished: unpublished);
var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList();
var contents = ids.Select(CreateContent).ToList();
var q = Q.Empty.WithIds(ids);
A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<List<ISchemaEntity>>.That.Matches(x => x.Count == 0), q, SearchScope.All))
.Returns(ResultList.Create(0, ids.Select(CreateContent)));
A.CallTo(() => contentRepository.QueryAsync(requestContext.App,
A<List<ISchemaEntity>>.That.Matches(x => x.Count == 1), q, scope))
.Returns(ResultList.Create(5, contents));
var result = await sut.QueryAsync(ctx, q);
var result = await sut.QueryAsync(requestContext, q);
Assert.Empty(result);
Assert.Equal(5, result.Total);
for (var i = 0; i < contents.Count; i++)
{
AssertContent(contents[i], result[i]);
}
}
[Fact]
public async Task QueryAll_should_only_query_only_users_contents_if_no_permission()
public async Task Should_query_contents_with_matching_permissions()
{
var ctx =
CreateContext(true, true, Permissions.AppContentsReadOwn);
var requestContext = CreateContext(allowSchema: false);
await sut.QueryAsync(ctx, schemaId.Name, Q.Empty);
var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList();
A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A<Q>.That.Matches(x => x.CreatedBy!.Equals(ctx.User.Token())), SearchScope.All))
.MustHaveHappened();
var q = Q.Empty.WithIds(ids);
A.CallTo(() => contentRepository.QueryAsync(requestContext.App,
A<List<ISchemaEntity>>.That.Matches(x => x.Count == 0), q, SearchScope.All))
.Returns(ResultList.Create(0, ids.Select(CreateContent)));
var result = await sut.QueryAsync(requestContext, q);
Assert.Empty(result);
}
[Fact]
public async Task QueryAll_should_query_all_contents_if_user_has_permission()
public async Task Should_query_contents_from_user_if_user_has_only_own_permission()
{
var ctx =
CreateContext(true, true, Permissions.AppContentsRead);
var requestContext = CreateContext(permissionId: Permissions.AppContentsReadOwn);
await sut.QueryAsync(ctx, schemaId.Name, Q.Empty);
await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty);
A.CallTo(() => contentRepository.QueryAsync(ctx.App, schema, A<Q>.That.Matches(x => x.CreatedBy == null), SearchScope.All))
A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema,
A<Q>.That.Matches(x => Equals(x.CreatedBy, requestContext.User.Token())), SearchScope.Published))
.MustHaveHappened();
}
[Theory]
[InlineData(1, 0, SearchScope.All)]
[InlineData(1, 1, SearchScope.All)]
[InlineData(0, 1, SearchScope.All)]
[InlineData(0, 0, SearchScope.Published)]
public async Task QueryAll_should_return_contents(int isFrontend, int unpublished, SearchScope scope)
[Fact]
public async Task Should_query_all_contents_if_user_has_read_permission()
{
var ctx =
CreateContext(isFrontend: isFrontend == 1, allowSchema: true).Clone(b => b
.WithUnpublished(unpublished == 1));
var requestContext = CreateContext(permissionId: Permissions.AppContentsRead);
var ids = Enumerable.Range(0, 5).Select(x => DomainId.NewGuid()).ToList();
var q = Q.Empty.WithIds(ids);
A.CallTo(() => contentRepository.QueryAsync(ctx.App, A<List<ISchemaEntity>>.That.Matches(x => x.Count == 1), q, scope))
.Returns(ResultList.Create(5, ids.Select(CreateContent)));
var result = await sut.QueryAsync(ctx, q);
await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty);
Assert.Equal(ids, result.Select(x => x.Id).ToList());
A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema,
A<Q>.That.Matches(x => x.CreatedBy == null), SearchScope.Published))
.MustHaveHappened();
}
private void SetupEnricher()
@ -276,12 +280,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
});
}
private Context CreateContext(bool isFrontend, bool allowSchema, string permissionId = Permissions.AppContentsRead)
private Context CreateContext(
int isFrontend = 0,
int isUnpublished = 0,
bool allowSchema = true,
string permissionId = Permissions.AppContentsRead)
{
var claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
if (isFrontend)
claimsIdentity.AddClaim(new Claim(OpenIdClaims.Subject, "user1"));
if (isFrontend == 1)
{
claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend));
}
@ -293,7 +303,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, concretePermission));
}
return new Context(claimsPrincipal, Mocks.App(appId));
return new Context(claimsPrincipal, Mocks.App(appId)).Clone(b => b.WithUnpublished(isUnpublished == 1));
}
private static void AssertContent(IContentEntity source, IEnrichedContentEntity? result)
{
Assert.NotNull(result);
Assert.NotSame(source, result);
Assert.Same(source.Data, result?.Data);
Assert.Equal(source.Id, result?.Id);
}
private IContentEntity CreateContent(DomainId id)

41
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferenceFluidExtensionTests.cs → backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs

@ -7,6 +7,7 @@
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
@ -17,18 +18,24 @@ using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ReferenceFluidExtensionTests
public class ReferencesFluidExtensionTests
{
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly FluidTemplateEngine sut;
public ReferenceFluidExtensionTests()
public ReferencesFluidExtensionTests()
{
var services =
new ServiceCollection()
.AddSingleton(appProvider)
.AddSingleton(contentQuery)
.BuildServiceProvider();
var extensions = new IFluidExtension[]
{
new ReferencesFluidExtension(contentQuery, appProvider)
new ReferencesFluidExtension(services)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false))
@ -43,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
var referenceId1 = DomainId.NewGuid();
var reference1 = CreateReference(referenceId1, 1);
var referenceId2 = DomainId.NewGuid();
var reference2 = CreateReference(referenceId1, 2);
var reference2 = CreateReference(referenceId2, 2);
var @event = new EnrichedContentEvent
{
@ -67,20 +74,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
};
var template = @"
{% for id in event.data.references.iv %}
{% for id in event.data.references.iv %}
{% reference 'ref', id %}
Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }}
{% endfor %}
";
Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} {{ ref.id }}
{% endfor %}
";
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
var expected = $@"
Text: Hello 1 World 1 {referenceId1}
Text: Hello 2 World 2 {referenceId2}
";
var result = await sut.RenderAsync(template, vars);
Assert.Equal(expected, result);
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index)
@ -98,5 +105,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
Id = referenceId
};
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Replace(" ", string.Empty);
}
}
}

150
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs

@ -0,0 +1,150 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ReferencesJintExtensionTests : IClassFixture<TranslationsFixture>
{
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app");
private readonly JintScriptEngine sut;
public ReferencesJintExtensionTests()
{
var services =
new ServiceCollection()
.AddSingleton(appProvider)
.AddSingleton(contentQuery)
.BuildServiceProvider();
var extensions = new IJintExtension[]
{
new ReferencesJintExtension(services)
};
A.CallTo(() => appProvider.GetAppAsync(appId.Id, false))
.Returns(Mocks.App(appId));
sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions);
}
[Fact]
public async Task Should_resolve_reference()
{
var referenceId1 = DomainId.NewGuid();
var reference1 = CreateReference(referenceId1, 1);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("references",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(referenceId1)));
A.CallTo(() => contentQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), A<Q>.That.HasIds(referenceId1)))
.Returns(ResultList.CreateFrom(1, reference1));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user };
var script = @"
getReference(data.references.iv[0], function (references) {
var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`;
complete(`${result1}`);
})";
var expected = @"
Text: Hello 1 World 1
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
[Fact]
public async Task Should_resolve_references()
{
var referenceId1 = DomainId.NewGuid();
var reference1 = CreateReference(referenceId1, 1);
var referenceId2 = DomainId.NewGuid();
var reference2 = CreateReference(referenceId1, 2);
var user = new ClaimsPrincipal();
var data =
new ContentData()
.AddField("references",
new ContentFieldData()
.AddJsonValue(JsonValue.Array(referenceId1, referenceId2)));
A.CallTo(() => contentQuery.QueryAsync(
A<Context>.That.Matches(x => x.App.Id == appId.Id && x.User == user), A<Q>.That.HasIds(referenceId1, referenceId2)))
.Returns(ResultList.CreateFrom(2, reference1, reference2));
var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user };
var script = @"
getReferences(data.references.iv, function (references) {
var result1 = `Text: ${references[0].data.field1.iv} ${references[0].data.field2.iv}`;
var result2 = `Text: ${references[1].data.field1.iv} ${references[1].data.field2.iv}`;
complete(`${result1}\n${result2}`);
})";
var expected = @"
Text: Hello 1 World 1
Text: Hello 2 World 2
";
var result = (await sut.ExecuteAsync(vars, script)).ToString();
Assert.Equal(Cleanup(expected), Cleanup(result));
}
private static IEnrichedContentEntity CreateReference(DomainId referenceId, int index)
{
return new ContentEntity
{
Data =
new ContentData()
.AddField("field1",
new ContentFieldData()
.AddJsonValue(JsonValue.Create($"Hello {index}")))
.AddField("field2",
new ContentFieldData()
.AddJsonValue(JsonValue.Create($"World {index}"))),
Id = referenceId
};
}
private static string Cleanup(string text)
{
return text
.Replace("\r", string.Empty)
.Replace("\n", string.Empty)
.Replace(" ", string.Empty);
}
}
}

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs

@ -36,11 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
return $"contents/{schemaId.Name}/{contentId}";
}
public string AppSettingsUI(NamedId<DomainId> appId)
{
throw new NotSupportedException();
}
public string AssetsUI(NamedId<DomainId> appId, string? query = null)
{
throw new NotSupportedException();

1
backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs

@ -9,7 +9,6 @@ using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;

Loading…
Cancel
Save