From c4f999b5ca33451a40785cd4cc1cfce8453caa7e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 21 Jun 2021 17:11:02 +0200 Subject: [PATCH] Asset tag. --- .../Squidex.Domain.Apps.Entities/AppTag.cs | 23 ++ .../Assets/AssetsFluidExtension.cs | 140 ++++++++--- .../Assets/AssetsJintExtension.cs | 85 +++++++ .../Contents/ReferencesFluidExtension.cs | 90 ++++--- .../MemoryStreamPooledObjectPolicy.cs | 2 +- .../src/Squidex.Web/Pipeline/AppResolver.cs | 1 - ...nitializer.cs => TokenStoreInitializer.cs} | 0 .../Assets/AssetsFluidExtensionTests.cs | 219 +++++++++++++++++- .../Assets/AssetsJintExtensionTests.cs | 182 ++++++++++++++- .../Contents/ReferencesFluidExtensionTests.cs | 48 +++- 10 files changed, 718 insertions(+), 72 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/AppTag.cs rename backend/src/Squidex/Areas/IdentityServer/Config/{TokenInitializer.cs => TokenStoreInitializer.cs} (100%) diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppTag.cs b/backend/src/Squidex.Domain.Apps.Entities/AppTag.cs new file mode 100644 index 000000000..5e8a5cca3 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/AppTag.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Fluid.Tags; +using Microsoft.Extensions.DependencyInjection; + +namespace Squidex.Domain.Apps.Entities +{ + internal abstract class AppTag : ArgumentsTag + { + protected IAppProvider AppProvider { get; } + + protected AppTag(IServiceProvider serviceProvider) + { + AppProvider = serviceProvider.GetRequiredService(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs index 9dfe59913..ff0c0a1b2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsFluidExtension.cs @@ -11,68 +11,52 @@ using System.Text.Encodings.Web; using System.Threading.Tasks; using Fluid; using Fluid.Ast; -using Fluid.Tags; +using Fluid.Values; using GraphQL.Utilities; using Microsoft.Extensions.DependencyInjection; 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; +using Squidex.Infrastructure.ObjectPool; namespace Squidex.Domain.Apps.Entities.Assets { public sealed class AssetsFluidExtension : IFluidExtension { + private static readonly FluidValue ErrorNullAsset = FluidValue.Create(null); + private static readonly FluidValue ErrorNoAsset = new StringValue("NoAsset"); + private static readonly FluidValue ErrorTooBig = new StringValue("ErrorTooBig"); private readonly IServiceProvider serviceProvider; - private sealed class AssetTag : ArgumentsTag + private sealed class AssetTag : AppTag { - private readonly IServiceProvider serviceProvider; + private readonly IAssetQueryService assetQuery; public AssetTag(IServiceProvider serviceProvider) + : base(serviceProvider) { - this.serviceProvider = serviceProvider; + assetQuery = serviceProvider.GetRequiredService(); } public override async ValueTask 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); + var id = await arguments[1].Expression.EvaluateAsync(context); - 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 content = await ResolveAssetAsync(AppProvider, assetQuery, enrichedEvent.AppId.Id, id); - var assetQuery = serviceProvider.GetRequiredService(); - - var asset = await assetQuery.FindAsync(requestContext, DomainId.Create(id)); - - if (asset != null) + if (content != null) { var name = (await arguments[0].Expression.EvaluateAsync(context)).ToStringValue(); - context.SetValue(name, asset); + context.SetValue(name, content); } } return Completion.Normal; } - - private Task GetAppAsync(EnrichedEvent enrichedEvent) - { - var appProvider = serviceProvider.GetRequiredService(); - - return appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); - } } public AssetsFluidExtension(IServiceProvider serviceProvider) @@ -91,11 +75,109 @@ namespace Squidex.Domain.Apps.Entities.Assets memberAccessStrategy.Register(); memberAccessStrategy.Register(); memberAccessStrategy.Register(); + + AddAssetFilter(); + AddAssetTextFilter(); + } + + private void AddAssetFilter() + { + var appProvider = serviceProvider.GetRequiredService(); + + var assetQuery = serviceProvider.GetRequiredService(); + + TemplateContext.GlobalFilters.AddAsyncFilter("asset", async (input, arguments, context) => + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) + { + var asset = await ResolveAssetAsync(appProvider, assetQuery, enrichedEvent.AppId.Id, input); + + if (asset == null) + { + return ErrorNullAsset; + } + + return FluidValue.Create(asset); + } + + return ErrorNullAsset; + }); + } + + private void AddAssetTextFilter() + { + var assetFileStore = serviceProvider.GetRequiredService(); + + TemplateContext.GlobalFilters.AddAsyncFilter("assetText", async (input, arguments, context) => + { + if (input is not ObjectValue objectValue) + { + return ErrorNoAsset; + } + + async Task ResolveAssetText(DomainId appId, DomainId id, long fileSize, long fileVersion) + { + if (fileSize > 256_000) + { + return ErrorTooBig; + } + + var tempStream = DefaultPools.MemoryStream.Get(); + try + { + await assetFileStore!.DownloadAsync(appId, id, fileVersion, tempStream); + + tempStream.Position = 0; + + using (var reader = new StreamReader(tempStream, leaveOpen: true)) + { + var text = reader.ReadToEnd(); + + return new StringValue(text); + } + } + finally + { + DefaultPools.MemoryStream.Return(tempStream); + } + } + + switch (objectValue.ToObjectValue()) + { + case IAssetEntity asset: + return await ResolveAssetText(asset.AppId.Id, asset.Id, asset.FileSize, asset.FileVersion); + + case EnrichedAssetEvent @event: + return await ResolveAssetText(@event.AppId.Id, @event.Id, @event.FileSize, @event.FileVersion); + } + + return ErrorNoAsset; + }); } public void RegisterLanguageExtensions(FluidParserFactory factory) { factory.RegisterTag("asset", new AssetTag(serviceProvider)); } + + private static async Task ResolveAssetAsync(IAppProvider appProvider, IAssetQueryService assetQuery, DomainId appId, FluidValue id) + { + var app = await appProvider.GetAppAsync(appId); + + if (app == null) + { + return null; + } + + var domainId = DomainId.Create(id.ToStringValue()); + + var requestContext = + Context.Admin(app).Clone(b => b + .WithoutTotal()); + + var asset = await assetQuery.FindAsync(requestContext, domainId); + + return asset; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs index 5682f2dcb..85011f098 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs @@ -7,15 +7,19 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Jint.Native; using Jint.Runtime; +using Jint.Runtime.Interop; using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; +using Squidex.Infrastructure.ObjectPool; using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Assets @@ -33,6 +37,12 @@ namespace Squidex.Domain.Apps.Entities.Assets } public void ExtendAsync(ExecutionContext context) + { + AddAssetText(context); + AddAsset(context); + } + + private void AddAsset(ExecutionContext context) { if (!context.TryGetValue(nameof(ScriptVars.AppId), out var appId)) { @@ -50,6 +60,81 @@ namespace Squidex.Domain.Apps.Entities.Assets context.Engine.SetValue("getAssets", action); } + private void AddAssetText(ExecutionContext context) + { + var action = new GetAssetsDelegate((references, callback) => GetAssetText(context, references, callback)); + + context.Engine.SetValue("getAssetText", action); + } + + private void GetAssetText(ExecutionContext context, JsValue input, Action callback) + { + GetAssetTextCore(context, input, callback).Forget(); + } + + private async Task GetAssetTextCore(ExecutionContext context, JsValue input, Action callback) + { + Guard.NotNull(callback, nameof(callback)); + + if (input is not ObjectWrapper objectWrapper) + { + callback(JsValue.FromObject(context.Engine, "ErrorNoAsset")); + return; + } + + async Task ResolveAssetText(DomainId appId, DomainId id, long fileSize, long fileVersion) + { + if (fileSize > 256_000) + { + callback(JsValue.FromObject(context.Engine, "ErrorTooBig")); + return; + } + + context.MarkAsync(); + + try + { + var assetFileStore = serviceProvider.GetRequiredService(); + + var tempStream = DefaultPools.MemoryStream.Get(); + try + { + await assetFileStore!.DownloadAsync(appId, id, fileVersion, tempStream, default, context.CancellationToken); + + tempStream.Position = 0; + + using (var reader = new StreamReader(tempStream, leaveOpen: true)) + { + var text = reader.ReadToEnd(); + + callback(JsValue.FromObject(context.Engine, text)); + } + } + finally + { + DefaultPools.MemoryStream.Return(tempStream); + } + } + catch (Exception ex) + { + context.Fail(ex); + } + } + + switch (objectWrapper.Target) + { + case IAssetEntity asset: + await ResolveAssetText(asset.AppId.Id, asset.Id, asset.FileSize, asset.FileVersion); + return; + + case EnrichedAssetEvent @event: + await ResolveAssetText(@event.AppId.Id, @event.Id, @event.FileSize, @event.FileVersion); + return; + } + + callback(JsValue.FromObject(context.Engine, "ErrorNoAsset")); + } + private void GetAssets(ExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) { GetReferencesAsync(context, appId, user, references, callback).Forget(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs index 67a427a6c..6b6c67abf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ReferencesFluidExtension.cs @@ -13,11 +13,10 @@ using System.Text.Encodings.Web; using System.Threading.Tasks; using Fluid; using Fluid.Ast; -using Fluid.Tags; +using Fluid.Values; using Microsoft.Extensions.DependencyInjection; 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 @@ -26,43 +25,26 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ReferencesFluidExtension : IFluidExtension { + private static readonly FluidValue ErrorNullReference = FluidValue.Create(null); private readonly IServiceProvider serviceProvider; - private sealed class ReferenceTag : ArgumentsTag + private sealed class ReferenceTag : AppTag { - private readonly IServiceProvider serviceProvider; + private readonly IContentQueryService contentQuery; public ReferenceTag(IServiceProvider serviceProvider) + : base(serviceProvider) { - this.serviceProvider = serviceProvider; + contentQuery = serviceProvider.GetRequiredService(); } public override async ValueTask 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); + var id = await arguments[1].Expression.EvaluateAsync(context); - if (app == null) - { - return Completion.Normal; - } - - var requestContext = - Context.Admin(app).Clone(b => b - .WithoutContentEnrichment() - .WithUnpublished() - .WithoutTotal()); - - var id = (await arguments[1].Expression.EvaluateAsync(context)).ToStringValue(); - - var domainId = DomainId.Create(id); - var domainIds = new List { domainId }; - - var contentQuery = serviceProvider.GetRequiredService(); - - var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds)); - var content = contents.FirstOrDefault(); + var content = await ResolveContentAsync(AppProvider, contentQuery, enrichedEvent.AppId.Id, id); if (content != null) { @@ -74,13 +56,6 @@ namespace Squidex.Domain.Apps.Entities.Contents return Completion.Normal; } - - private Task GetAppAsync(EnrichedEvent enrichedEvent) - { - var appProvider = serviceProvider.GetRequiredService(); - - return appProvider.GetAppAsync(enrichedEvent.AppId.Id, false); - } } public ReferencesFluidExtension(IServiceProvider serviceProvider) @@ -98,11 +73,60 @@ namespace Squidex.Domain.Apps.Entities.Contents memberAccessStrategy.Register(); memberAccessStrategy.Register(); memberAccessStrategy.Register(); + + AddReferenceFilter(); + } + + private void AddReferenceFilter() + { + var appProvider = serviceProvider.GetRequiredService(); + + var contentQuery = serviceProvider.GetRequiredService(); + + TemplateContext.GlobalFilters.AddAsyncFilter("reference", async (input, arguments, context) => + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent enrichedEvent) + { + var content = await ResolveContentAsync(appProvider, contentQuery, enrichedEvent.AppId.Id, input); + + if (content == null) + { + return ErrorNullReference; + } + + return FluidValue.Create(content); + } + + return ErrorNullReference; + }); } public void RegisterLanguageExtensions(FluidParserFactory factory) { factory.RegisterTag("reference", new ReferenceTag(serviceProvider)); } + + private static async Task ResolveContentAsync(IAppProvider appProvider, IContentQueryService contentQuery, DomainId appId, FluidValue id) + { + var app = await appProvider.GetAppAsync(appId); + + if (app == null) + { + return null; + } + + var domainId = DomainId.Create(id.ToStringValue()); + var domainIds = new List { domainId }; + + var requestContext = + Context.Admin(app).Clone(b => b + .WithUnpublished() + .WithoutTotal()); + + var contents = await contentQuery.QueryAsync(requestContext, Q.Empty.WithIds(domainIds)); + var content = contents.FirstOrDefault(); + + return content; + } } } diff --git a/backend/src/Squidex.Infrastructure/ObjectPool/MemoryStreamPooledObjectPolicy.cs b/backend/src/Squidex.Infrastructure/ObjectPool/MemoryStreamPooledObjectPolicy.cs index 53a44fc6a..5370cb0d1 100644 --- a/backend/src/Squidex.Infrastructure/ObjectPool/MemoryStreamPooledObjectPolicy.cs +++ b/backend/src/Squidex.Infrastructure/ObjectPool/MemoryStreamPooledObjectPolicy.cs @@ -33,4 +33,4 @@ namespace Squidex.Infrastructure.ObjectPool return true; } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index 1e3bb67e9..87eb0644b 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/TokenInitializer.cs b/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs similarity index 100% rename from backend/src/Squidex/Areas/IdentityServer/Config/TokenInitializer.cs rename to backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs index 095354905..8021bd2b3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -5,10 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.DependencyInjection; +using Squidex.Assets; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Templates; @@ -22,6 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public class AssetsFluidExtensionTests { private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly FluidTemplateEngine sut; @@ -32,6 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Assets new ServiceCollection() .AddSingleton(appProvider) .AddSingleton(assetQuery) + .AddSingleton(assetFileStore) .BuildServiceProvider(); var extensions = new IFluidExtension[] @@ -79,7 +84,53 @@ namespace Squidex.Domain.Apps.Entities.Assets {% 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)); + } + + [Fact] + public async Task Should_resolve_assets_in_loop_with_filter() + { + 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() + .AddInvariant(JsonValue.Array(assetId1, assetId2))), + AppId = appId + }; + + A.CallTo(() => assetQuery.FindAsync(A._, assetId1, EtagVersion.Any, A._)) + .Returns(asset1); + + A.CallTo(() => assetQuery.FindAsync(A._, assetId2, EtagVersion.Any, A._)) + .Returns(asset2); + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" + {% for id in event.data.assets.iv %} + {% assign ref = id | asset %} + Text: {{ ref.fileName }} {{ ref.id }} + {% endfor %} + "; var expected = $@" Text: file1.jpg {assetId1} @@ -91,9 +142,171 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(Cleanup(expected), Cleanup(result)); } - private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index) + [Fact] + public async Task Should_resolve_asset_text() { - return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId }; + var assetId = DomainId.NewGuid(); + var asset = CreateAsset(assetId, 1); + + var @event = new EnrichedContentEvent + { + Data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(assetId))), + AppId = appId + }; + + A.CallTo(() => assetQuery.FindAsync(A._, assetId, EtagVersion.Any, A._)) + .Returns(asset); + + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, asset.FileVersion, A._, A._, A._)) + .Invokes(x => + { + var stream = x.GetArgument(3)!; + + stream.Write(Encoding.UTF8.GetBytes("Hello Asset")); + }); + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" + {% assign ref = event.data.assets.iv[0] | asset %} + Text: {{ ref | assetText }} + "; + + var expected = $@" + Text: Hello Asset + "; + + var result = await sut.RenderAsync(template, vars); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_not_resolve_asset_text_if_too_big() + { + var assetId = DomainId.NewGuid(); + var asset = CreateAsset(assetId, 1, 1_000_000); + + var @event = new EnrichedContentEvent + { + Data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(assetId))), + AppId = appId + }; + + A.CallTo(() => assetQuery.FindAsync(A._, assetId, EtagVersion.Any, A._)) + .Returns(asset); + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" + {% assign ref = event.data.assets.iv[0] | asset %} + Text: {{ ref | assetText }} + "; + + var expected = $@" + Text: ErrorTooBig + "; + + var result = await sut.RenderAsync(template, vars); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + + A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_resolve_asset_text_from_event() + { + var @event = new EnrichedAssetEvent + { + Id = DomainId.NewGuid(), + FileVersion = 0, + FileSize = 100, + AppId = appId + }; + + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, @event.Id, @event.FileVersion, A._, A._, A._)) + .Invokes(x => + { + var stream = x.GetArgument(3)!; + + stream.Write(Encoding.UTF8.GetBytes("Hello Asset")); + }); + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" + Text: {{ event | assetText }} + "; + + var expected = $@" + Text: Hello Asset + "; + + var result = await sut.RenderAsync(template, vars); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_resolve_asset_text_from_event_if_too_big() + { + var @event = new EnrichedAssetEvent + { + Id = DomainId.NewGuid(), + FileVersion = 0, + FileSize = 1_000_000, + AppId = appId + }; + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" + Text: {{ event | assetText }} + "; + + var expected = $@" + Text: ErrorTooBig + "; + + var result = await sut.RenderAsync(template, vars); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + + A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + + private IEnrichedAssetEntity CreateAsset(DomainId assetId, int index, int fileSize = 100) + { + return new AssetEntity + { + AppId = appId, + Id = assetId, + FileSize = fileSize, + FileName = $"file{index}.jpg", + }; } private static string Cleanup(string text) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs index 2fcc0e93d..f4e749d4e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -6,14 +6,18 @@ // ========================================================================== using System; +using System.IO; using System.Security.Claims; +using System.Text; using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Squidex.Assets; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -26,6 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public class AssetsJintExtensionTests : IClassFixture { private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly JintScriptEngine sut; @@ -36,6 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Assets new ServiceCollection() .AddSingleton(appProvider) .AddSingleton(assetQuery) + .AddSingleton(assetFileStore) .BuildServiceProvider(); var extensions = new IJintExtension[] @@ -78,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result1 = `Text: ${assets[0].fileName}`; complete(`${result1}`); - })"; + });"; var expected = @" Text: file1.jpg @@ -117,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var result2 = `Text: ${assets[1].fileName}`; complete(`${result1}\n${result2}`); - })"; + });"; var expected = @" Text: file1.jpg @@ -129,9 +135,177 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(Cleanup(expected), Cleanup(result)); } - private static IEnrichedAssetEntity CreateAsset(DomainId assetId, int index) + [Fact] + public async Task Should_resolve_asset_text() + { + var assetId = DomainId.NewGuid(); + var asset = CreateAsset(assetId, 1); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(assetId))); + + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetId), A._)) + .Returns(ResultList.CreateFrom(2, asset)); + + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, asset.FileVersion, A._, A._, A._)) + .Invokes(x => + { + var stream = x.GetArgument(3)!; + + stream.Write(Encoding.UTF8.GetBytes("Hello Asset")); + }); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getAssets(data.assets.iv, function (assets) { + getAssetText(assets[0], function (text) { + var result = `Text: ${text}`; + + complete(result); + }); + });"; + + var expected = @" + Text: Hello Asset + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_not_resolve_asset_text_if_too_big() + { + var assetId = DomainId.NewGuid(); + var asset = CreateAsset(assetId, 1, 1_000_000); + + var user = new ClaimsPrincipal(); + + var data = + new ContentData() + .AddField("assets", + new ContentFieldData() + .AddInvariant(JsonValue.Array(assetId))); + + A.CallTo(() => assetQuery.QueryAsync( + A.That.Matches(x => x.App.Id == appId.Id && x.User == user), null, A.That.HasIds(assetId), A._)) + .Returns(ResultList.CreateFrom(2, asset)); + + var vars = new ScriptVars { Data = data, AppId = appId.Id, User = user }; + + var script = @" + getAssets(data.assets.iv, function (assets) { + getAssetText(assets[0], function (text) { + var result = `Text: ${text}`; + + complete(result); + }); + });"; + + var expected = @" + Text: ErrorTooBig + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + + A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_resolve_asset_text_from_event() { - return new AssetEntity { FileName = $"file{index}.jpg", Id = assetId }; + var @event = new EnrichedAssetEvent + { + Id = DomainId.NewGuid(), + FileVersion = 0, + FileSize = 100, + AppId = appId + }; + + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, @event.Id, @event.FileVersion, A._, A._, A._)) + .Invokes(x => + { + var stream = x.GetArgument(3)!; + + stream.Write(Encoding.UTF8.GetBytes("Hello Asset")); + }); + + var vars = new ScriptVars + { + ["event"] = @event + }; + + var script = @" + getAssetText(event, function (text) { + var result = `Text: ${text}`; + + complete(result); + });"; + + var expected = @" + Text: Hello Asset + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_resolve_asset_text_from_event_if_too_big() + { + var @event = new EnrichedAssetEvent + { + Id = DomainId.NewGuid(), + FileVersion = 0, + FileSize = 1_000_000, + AppId = appId + }; + + var vars = new ScriptVars + { + ["event"] = @event + }; + + var script = @" + getAssetText(event, function (text) { + var result = `Text: ${text}`; + + complete(result); + });"; + + var expected = @" + Text: ErrorTooBig + "; + + var result = (await sut.ExecuteAsync(vars, script)).ToString(); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + + A.CallTo(() => assetFileStore.DownloadAsync(A._, A._, A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + + private IEnrichedAssetEntity CreateAsset(DomainId assetId, int index, int fileSize = 100) + { + return new AssetEntity + { + AppId = appId, + Id = assetId, + FileSize = fileSize, + FileName = $"file{index}.jpg", + }; } private static string Cleanup(string text) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs index 3df3d5681..9fcf9b253 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs @@ -79,7 +79,53 @@ namespace Squidex.Domain.Apps.Entities.Contents {% reference 'ref', id %} Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} {{ ref.id }} {% endfor %} - "; + "; + + var expected = $@" + Text: Hello 1 World 1 {referenceId1} + Text: Hello 2 World 2 {referenceId2} + "; + + var result = await sut.RenderAsync(template, vars); + + Assert.Equal(Cleanup(expected), Cleanup(result)); + } + + [Fact] + public async Task Should_resolve_references_in_loop_with_filter() + { + var referenceId1 = DomainId.NewGuid(); + var reference1 = CreateReference(referenceId1, 1); + var referenceId2 = DomainId.NewGuid(); + var reference2 = CreateReference(referenceId2, 2); + + var @event = new EnrichedContentEvent + { + Data = + new ContentData() + .AddField("references", + new ContentFieldData() + .AddInvariant(JsonValue.Array(referenceId1, referenceId2))), + AppId = appId + }; + + A.CallTo(() => contentQuery.QueryAsync(A._, A.That.HasIds(referenceId1), A._)) + .Returns(ResultList.CreateFrom(1, reference1)); + + A.CallTo(() => contentQuery.QueryAsync(A._, A.That.HasIds(referenceId2), A._)) + .Returns(ResultList.CreateFrom(1, reference2)); + + var vars = new TemplateVars + { + ["event"] = @event + }; + + var template = @" + {% for id in event.data.references.iv %} + {% assign ref = id | reference %} + Text: {{ ref.data.field1.iv }} {{ ref.data.field2.iv }} {{ ref.id }} + {% endfor %} + "; var expected = $@" Text: Hello 1 World 1 {referenceId1}