From 91c1532ef7991d1a2ebfa08581ca93c08a396d61 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 5 Feb 2021 14:13:25 +0100 Subject: [PATCH] Fix surrogate keys. --- .../Assets/MongoAssetRepository.cs | 2 +- .../Assets/IAssetQueryService.cs | 6 +- .../Assets/Queries/AssetQueryService.cs | 160 ++++++++++---- .../Assets/Repositories/IAssetRepository.cs | 6 +- .../Contents/Queries/ContentQueryService.cs | 73 +++---- .../Assets/AssetContentController.cs | 20 +- .../Assets/MongoDb/AssetsQueryTests.cs | 2 +- .../Assets/Queries/AssetQueryServiceTests.cs | 174 +++++++++++++--- .../Queries/ContentQueryServiceTests.cs | 196 ++++++++++-------- 9 files changed, 423 insertions(+), 216 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 729994f66..6df8b75a4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - public async Task FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize) + public async Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize) { using (Profiler.TraceMethod()) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index 2d4945aa9..d3db4381b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -21,6 +21,10 @@ namespace Squidex.Domain.Apps.Entities.Assets Task FindByHashAsync(Context context, string hash, string fileName, long fileSize); - Task FindAsync(Context context, DomainId id); + Task FindAsync(Context context, DomainId id, long version = EtagVersion.Any); + + Task FindBySlugAsync(Context context, string slug); + + Task FindGlobalAsync(Context context, DomainId id); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs index 3919afd6b..af9d40e63 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -10,107 +10,195 @@ 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 EmptyAssets = ResultList.CreateFrom(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 FindByHashAsync(Context context, string hash, string fileName, long fileSize) + public async Task> FindAssetFolderAsync(DomainId appId, DomainId id) { - Guard.NotNull(context, nameof(context)); + using (Profiler.TraceMethod()) + { + var result = new List(); - var asset = await assetRepository.FindAssetAsync(context.App.Id, hash, fileName, fileSize); + while (id != DomainId.Empty) + { + var folder = await assetFolderRepository.FindAssetFolderAsync(appId, id); - if (asset != null) - { - return await assetEnricher.EnrichAsync(asset, context); + 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> QueryAssetFoldersAsync(Context context, DomainId parentId) + { + using (Profiler.TraceMethod()) + { + var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId); - return null; + return assetFolders; + } } - public async Task FindAsync(Context context, DomainId id) + public async Task FindByHashAsync(Context context, string hash, string fileName, long fileSize) { Guard.NotNull(context, nameof(context)); - var asset = await assetRepository.FindAssetAsync(context.App.Id, id); - - if (asset != null) + using (Profiler.TraceMethod()) { - return await assetEnricher.EnrichAsync(asset, context); - } + var asset = await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize); + + if (asset == null) + { + return null; + } - return null; + return await TransformAsync(context, asset); + } } - public async Task> FindAssetFolderAsync(DomainId appId, DomainId id) + public async Task FindBySlugAsync(Context context, string slug) { - var result = new List(); + Guard.NotNull(context, nameof(context)); - while (id != DomainId.Empty) + using (Profiler.TraceMethod()) { - var folder = await assetFolderRepository.FindAssetFolderAsync(appId, id); + var asset = await assetRepository.FindAssetBySlugAsync(context.App.Id, slug); - 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); } + } + + public async Task FindGlobalAsync(Context context, DomainId id) + { + Guard.NotNull(context, nameof(context)); + + using (Profiler.TraceMethod()) + { + var asset = await assetRepository.FindAssetAsync(id); + + if (asset == null) + { + return null; + } - return result; + return await TransformAsync(context, asset); + } } - public async Task> QueryAssetFoldersAsync(Context context, DomainId parentId) + public async Task FindAsync(Context context, DomainId id, long version = EtagVersion.Any) { - var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId); + Guard.NotNull(context, nameof(context)); - return assetFolders; + using (Profiler.TraceMethod()) + { + IAssetEntity? asset; + + if (version > EtagVersion.Empty) + { + asset = await assetLoader.GetAsync(context.App.Id, id, version); + } + else + { + asset = await assetRepository.FindAssetAsync(context.App.Id, id); + } + + if (asset == null) + { + return null; + } + + return await TransformAsync(context, asset); + } } public async Task> QueryAsync(Context context, DomainId? parentId, Q q) { Guard.NotNull(context, nameof(context)); - Guard.NotNull(q, nameof(q)); - q = await queryParser.ParseQueryAsync(context, q); - - var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q); + if (q == null) + { + return EmptyAssets; + } - if (q.Ids != null && q.Ids.Count > 0) + using (Profiler.TraceMethod()) { - assets = assets.SortSet(x => x.Id, q.Ids); + q = await queryParser.ParseQueryAsync(context, q); + + var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q); + + if (q.Ids != null && q.Ids.Count > 0) + { + assets = assets.SortSet(x => x.Id, q.Ids); + } + + return await TransformAsync(context, assets); } + } - var enriched = await assetEnricher.EnrichAsync(assets, context); + private async Task> TransformAsync(Context context, IResultList assets) + { + var transformed = await TransformCoreAsync(context, assets); - return ResultList.Create(assets.Total, enriched); + return ResultList.Create(assets.Total, transformed); + } + + private async Task TransformAsync(Context context, IAssetEntity asset) + { + var transformed = await TransformCoreAsync(context, Enumerable.Repeat(asset, 1)); + + return transformed[0]; + } + + private async Task> TransformCoreAsync(Context context, IEnumerable assets) + { + using (Profiler.TraceMethod()) + { + return await assetEnricher.EnrichAsync(assets, context); + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index d50fe4bc7..ed20b6fcb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -21,12 +21,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories Task> QueryChildIdsAsync(DomainId appId, DomainId parentId); - Task FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize); + Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize); + + Task FindAssetBySlugAsync(DomainId appId, string slug); Task FindAssetAsync(DomainId appId); Task FindAssetAsync(DomainId appId, DomainId id); - - Task FindAssetBySlugAsync(DomainId appId, string slug); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index ae554b021..0424ae92e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/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 FindAsync(Context context, string schemaIdOrName, DomainId id, long version = -1) + public async Task FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any) { Guard.NotNull(context, nameof(context)); - if (id == default) - { - throw new DomainObjectNotFoundException(id.ToString()); - } - - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - using (Profiler.TraceMethod()) { + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + 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,20 +81,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { Guard.NotNull(context, nameof(context)); - if (q == null) + using (Profiler.TraceMethod()) { - return EmptyContents; - } + if (q == null) + { + return EmptyContents; + } - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); - if (!HasPermission(context, schema, Permissions.AppContentsRead)) - { - q.CreatedBy = context.User.Token(); - } + if (!HasPermission(context, schema, Permissions.AppContentsRead)) + { + q.CreatedBy = context.User.Token(); + } - using (Profiler.TraceMethod()) - { q = await queryParser.ParseAsync(context, q, schema); var contents = await contentRepository.QueryAsync(context.App, schema, q, context.Scope()); @@ -117,20 +112,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { Guard.NotNull(context, nameof(context)); - if (q == null) + using (Profiler.TraceMethod()) { - return EmptyContents; - } + if (q == null) + { + return EmptyContents; + } - var schemas = await GetSchemasAsync(context); + var schemas = await GetSchemasAsync(context); - if (schemas.Count == 0) - { - return EmptyContents; - } + if (schemas.Count == 0) + { + return EmptyContents; + } - using (Profiler.TraceMethod()) - { 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.Permissions.Allows(permissionId, context.App.Name, schema.SchemaDef.Name); } - - private Task FindCoreAsync(Context context, DomainId id, ISchemaEntity schema) - { - return contentRepository.FindContentAsync(context.App, schema, id, context.Scope()); - } - - private Task FindByVersionAsync(Context context, DomainId id, long version) - { - return contentVersionLoader.GetAsync(context.App.Id, id, version); - } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 947049964..820364ded 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -17,7 +17,6 @@ using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Assets; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Log; @@ -34,23 +33,20 @@ namespace Squidex.Areas.Api.Controllers.Assets public sealed class AssetContentController : ApiController { private readonly IAssetFileStore assetFileStore; - private readonly IAssetRepository assetRepository; - private readonly IAssetLoader assetLoader; + private readonly IAssetQueryService assetQuery; private readonly IAssetStore assetStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; public AssetContentController( ICommandBus commandBus, IAssetFileStore assetFileStore, - IAssetRepository assetRepository, - IAssetLoader assetLoader, + IAssetQueryService assetQuery, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator) : base(commandBus) { this.assetFileStore = assetFileStore; - this.assetRepository = assetRepository; - this.assetLoader = assetLoader; + this.assetQuery = assetQuery; this.assetStore = assetStore; this.assetThumbnailGenerator = assetThumbnailGenerator; } @@ -74,11 +70,11 @@ namespace Squidex.Areas.Api.Controllers.Assets [AllowAnonymous] public async Task GetAssetContentBySlug(string app, string idOrSlug, [FromQuery] AssetContentQueryDto queries, string? more = null) { - var asset = await assetRepository.FindAssetAsync(AppId, DomainId.Create(idOrSlug)); + var asset = await assetQuery.FindAsync(Context, DomainId.Create(idOrSlug)); if (asset == null) { - asset = await assetRepository.FindAssetBySlugAsync(AppId, idOrSlug); + asset = await assetQuery.FindBySlugAsync(Context, idOrSlug); } return await DeliverAssetAsync(asset, queries); @@ -102,7 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [Obsolete("Use overload with app name")] public async Task GetAssetContent(DomainId id, [FromQuery] AssetContentQueryDto queries) { - var asset = await assetRepository.FindAssetAsync(id); + var asset = await assetQuery.FindGlobalAsync(Context, id); return await DeliverAssetAsync(asset, queries); } @@ -123,9 +119,9 @@ namespace Squidex.Areas.Api.Controllers.Assets return StatusCode(403); } - if (asset != null && queries.Version > EtagVersion.Any && asset.Version != queries.Version) + if (asset != null && queries.Version > EtagVersion.Any && asset.Version != queries.Version && Context.App != null) { - asset = await assetLoader.GetAsync(asset.AppId.Id, asset.Id, queries.Version); + asset = await assetQuery.FindAsync(Context, asset.Id, queries.Version); } if (asset == null) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs index aeec3db70..81c0af1c8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs +++ b/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); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index ec32c9dc2..6a0ff152b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/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(); private readonly IAssetRepository assetRepository = A.Fake(); + private readonly IAssetLoader assetLoader = A.Fake(); private readonly IAssetFolderRepository assetFolderRepository = A.Fake(); private readonly NamedId 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.ParseQueryAsync(requestContext, A._)) .ReturnsLazily(c => Task.FromResult(c.GetArgument(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(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()); - var enriched = new AssetEntity(); + A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id)) + .Returns(asset); - 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); + AssertAsset(asset, result); + } - var result = await sut.FindAsync(requestContext, found.Id); + [Fact] + public async Task Should_return_null_if_asset_by_id_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id)) + .Returns(Task.FromResult(null)); - Assert.Same(enriched, result); + var result = await sut.FindAsync(requestContext, asset.Id); + + Assert.Null(result); } [Fact] - public async Task Should_find_assets_by_hash_and_and_enrich_it() + public async Task Should_find_asset_by_id_and_version_and_enrich_it() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2)) + .Returns(asset); + + var result = await sut.FindAsync(requestContext, asset.Id, 2); + + AssertAsset(asset, result); + } + + [Fact] + public async Task Should_return_null_if_asset_by_id_and_version_cannot_be_found() + { + var asset = CreateAsset(DomainId.NewGuid()); + + A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2)) + .Returns(Task.FromResult(null)); + + var result = await sut.FindAsync(requestContext, asset.Id, 2); + + Assert.Null(result); + } + + [Fact] + public async Task Should_find_global_asset_by_id_and_enrich_it() { - var found = new AssetEntity { Id = DomainId.NewGuid() }; + var asset = CreateAsset(DomainId.NewGuid()); - var enriched = new AssetEntity(); + A.CallTo(() => assetRepository.FindAssetAsync(asset.Id)) + .Returns(asset); - A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, "hash", "name", 123)) - .Returns(found); + var result = await sut.FindGlobalAsync(requestContext, asset.Id); - A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext)) - .Returns(enriched); + 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(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()); - var enriched1 = new AssetEntity(); - var enriched2 = new AssetEntity(); + A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123)) + .Returns(Task.FromResult(null)); + + 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>.That.IsSameSequenceAs(found1, found2), requestContext)) - .Returns(new List { 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(); @@ -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>._, A._)) + .ReturnsLazily(x => + { + var input = x.GetArgument>(0)!; + + return Task.FromResult>(input.Select(c => SimpleMapper.Map(c, new AssetEntity())).ToList()); + }); + } } } \ No newline at end of file diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index bdfe78f1d..c7bbf0882 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/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(); private readonly IContentLoader contentVersionLoader = A.Fake(); private readonly ISchemaEntity schema; - private readonly DomainId contentId = DomainId.NewGuid(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId 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(); 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._, A._, true)) .Returns((ISchemaEntity?)null); - await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(ctx, schemaId.Name)); + await Assert.ThrowsAsync(() => 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._)) - .Returns(CreateContent(contentId)); + var content = CreateContent(DomainId.NewGuid()); - await Assert.ThrowsAsync(() => sut.FindAsync(ctx, schemaId.Name, contentId)); + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A._)) + .Returns(CreateContent(DomainId.NewGuid())); + + await Assert.ThrowsAsync(() => 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._)) + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A._)) .Returns(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) - .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(() => sut.QueryAsync(ctx, schemaId.Name, Q.Empty)); + await Assert.ThrowsAsync(() => 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) - .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>.That.Matches(x => x.Count == 0), q, SearchScope.All)) - .Returns(ResultList.Create(0, ids.Select(CreateContent))); + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, + A>.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.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>.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.That.Matches(x => x.CreatedBy == null), SearchScope.All)) + A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, + A.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) - .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>.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.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)).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)