From f96dfb0ddd6e2ae26c5ed0aacd38bc35ea0df20c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 25 Sep 2020 14:18:04 +0200 Subject: [PATCH] More tests around custom ids. --- backend/i18n/source/backend_en.json | 1 + .../Assets/AssetCommandMiddleware.cs | 19 ++-- .../Assets/Commands/CreateAsset.cs | 2 + .../Contents/BulkUpdateCommandMiddleware.cs | 1 - .../EventSourcing/MongoEventStore.cs | 9 +- .../Commands/DomainObjectBase.cs | 14 +-- .../DomainObjectConflictException.cs | 32 +++++++ backend/src/Squidex.Shared/Texts.it.resx | 3 + backend/src/Squidex.Shared/Texts.nl.resx | 3 + backend/src/Squidex.Shared/Texts.resx | 3 + .../src/Squidex.Web/ApiExceptionConverter.cs | 3 + .../ApiExceptionFilterAttribute.cs | 3 +- .../Controllers/Assets/AssetsController.cs | 11 ++- .../Assets/AssetCommandMiddlewareTests.cs | 15 ++++ .../Commands/DomainObjectTests.cs | 54 ++++++++++-- .../Commands/LogSnapshotDomainObjectTests.cs | 44 +++++++++- .../ApiExceptionFilterAttributeTests.cs | 13 +++ .../TestSuite.ApiTests/AssetTests.cs | 41 +++++++++ .../TestSuite.ApiTests/ContentUpdateTests.cs | 87 +++++++++++++++++-- .../TestSuite.Shared/Fixtures/AssetFixture.cs | 4 +- .../TestSuite.Shared/TestSuite.Shared.csproj | 6 +- backend/tools/TestSuite/TestSuite.sln | 6 ++ 22 files changed, 334 insertions(+), 40 deletions(-) create mode 100644 backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index cd11ee5b7..e8451a51f 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -200,6 +200,7 @@ "dotnet_identity_UserLockedOut": "User is locked out.", "exception.invalidJsonQuery": "Json query not valid: {message}", "exception.invalidJsonQueryJson": "Json query not valid json: {message}", + "exceptions.domainObjectConflict": "Entity ({id}) already exists.", "exceptions.domainObjectDeleted": "Entity ({id}) has been deleted.", "exceptions.domainObjectNotFound": "Entity ({id}) does not exist.", "exceptions.domainObjectVersion": "Entity ({id}) requested version {expectedVersion}, but found {currentVersion}.", diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index 8d2649b2d..acc4c6f89 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -60,18 +60,21 @@ namespace Squidex.Domain.Apps.Entities.Assets var ctx = contextProvider.Context.Clone().WithoutAssetEnrichment(); - var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); - - foreach (var existing in existings) + if (!createAsset.Duplicate) { - if (IsDuplicate(existing, createAsset.File)) + var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); + + foreach (var existing in existings) { - var result = new AssetCreatedResult(existing, true); + if (IsDuplicate(existing, createAsset.File)) + { + var result = new AssetCreatedResult(existing, true); - context.Complete(result); + context.Complete(result); - await next(context); - return; + await next(context); + return; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs index 5d4731817..6b11706b3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs @@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands public HashSet Tags { get; } = new HashSet(); + public bool Duplicate { get; set; } + public CreateAsset() { AssetId = DomainId.NewGuid(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs index 39e68ac8d..55591e57a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs @@ -9,7 +9,6 @@ using System; using System.Linq; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; -using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index fde1d0a57..33eef572b 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using MongoDB.Driver.Core.Clusters; using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.EventSourcing @@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.EventSourcing get { return Collection; } } - public bool IsReplicaSet { get; } + public bool IsReplicaSet { get; private set; } public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) : base(database) @@ -51,9 +52,9 @@ namespace Squidex.Infrastructure.EventSourcing return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; } - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) { - return collection.Indexes.CreateManyAsync(new[] + await collection.Indexes.CreateManyAsync(new[] { new CreateIndexModel( Index @@ -75,6 +76,8 @@ namespace Squidex.Infrastructure.EventSourcing Unique = true }) }, ct); + + IsReplicaSet = Database.Client.Cluster.Description.Type == ClusterType.ReplicaSet; } } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 8c276f25e..20d08a311 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -146,10 +146,7 @@ namespace Squidex.Infrastructure.Commands Guard.NotNull(command, nameof(command)); Guard.NotNull(handler, nameof(handler)); - if (isUpdate) - { - await EnsureLoadedAsync(); - } + await EnsureLoadedAsync(); if (IsDeleted()) { @@ -160,14 +157,19 @@ namespace Squidex.Infrastructure.Commands { if (!CanAccept(command)) { - throw new NotSupportedException("Invalid command."); + throw new DomainException("Invalid command."); } } else { + if (Version > EtagVersion.Empty) + { + throw new DomainObjectConflictException(uniqueId.ToString()); + } + if (!CanAcceptCreation(command)) { - throw new NotSupportedException("Invalid command."); + throw new DomainException("Invalid command."); } } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs b/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs new file mode 100644 index 000000000..189769c4d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Runtime.Serialization; +using Squidex.Infrastructure.Translations; + +namespace Squidex.Infrastructure +{ + [Serializable] + public class DomainObjectConflictException : DomainObjectException + { + public DomainObjectConflictException(string id, Exception? inner = null) + : base(FormatMessage(id), id, inner) + { + } + + protected DomainObjectConflictException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private static string FormatMessage(string id) + { + return T.Get("exceptions.domainObjectDeleted", new { id }); + } + } +} diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 08b32a3b5..9e3252b56 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -685,6 +685,9 @@ La query Json non è valida: {message} + + Entity ({id}) already exists. + L'entità ({id}) è stata cancellata. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index 0b5bfc1f0..e42015e7e 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -685,6 +685,9 @@ Json-query is niet geldig json: {message} + + Entity ({id}) already exists. + Entiteit ({id}) is verwijderd. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 9755c2e6d..f6c05d539 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -685,6 +685,9 @@ Json query not valid json: {message} + + Entity ({id}) already exists. + Entity ({id}) has been deleted. diff --git a/backend/src/Squidex.Web/ApiExceptionConverter.cs b/backend/src/Squidex.Web/ApiExceptionConverter.cs index 605d7633c..40691cf6e 100644 --- a/backend/src/Squidex.Web/ApiExceptionConverter.cs +++ b/backend/src/Squidex.Web/ApiExceptionConverter.cs @@ -91,6 +91,9 @@ namespace Squidex.Web case DomainObjectVersionException _: return (CreateError(412, exception.Message), true); + case DomainObjectConflictException _: + return (CreateError(409, exception.Message), true); + case DomainForbiddenException _: return (CreateError(403, exception.Message), true); diff --git a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs index 0300af0b9..f2d603a11 100644 --- a/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs +++ b/backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs @@ -35,7 +35,8 @@ namespace Squidex.Web { var log = context.HttpContext.RequestServices.GetService(); - log.LogError(context.Exception, w => w.WriteProperty("message", "An unexpected exception has occurred.")); + log.LogError(context.Exception, w => w + .WriteProperty("message", "An unexpected exception has occurred.")); } context.Result = GetResult(error); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 1649ecebf..85bf0681d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -172,6 +172,8 @@ namespace Squidex.Areas.Api.Controllers.Assets /// The name of the app. /// The optional parent folder id. /// The file to upload. + /// The optional custom asset id. + /// True to duplicate the asset, event if the file has been uploaded. /// /// 201 => Asset created. /// 404 => App not found. @@ -186,11 +188,16 @@ namespace Squidex.Areas.Api.Controllers.Assets [AssetRequestSizeLimit] [ApiPermissionOrAnonymous(Permissions.AppAssetsCreate)] [ApiCosts(1)] - public async Task PostAsset(string app, [FromQuery] string parentId, IFormFile file) + public async Task PostAsset(string app, [FromQuery] string parentId, IFormFile file, [FromQuery] string? id = null, [FromQuery] bool duplicate = false) { var assetFile = await CheckAssetFileAsync(file); - var command = new CreateAsset { File = assetFile, ParentId = parentId }; + var command = new CreateAsset { File = assetFile, ParentId = parentId, Duplicate = duplicate }; + + if (!string.IsNullOrWhiteSpace(id)) + { + command.AssetId = id; + } var response = await InvokeCommandAsync(command); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 9b064e17e..d404f0d33 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -175,6 +175,21 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.True(result.IsDuplicate); } + [Fact] + public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_found_but_duplicate_allowed() + { + var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file, Duplicate = true }); + var context = CreateContextForCommand(command); + + SetupSameHashAsset(file.FileName, file.FileSize, out _); + + await sut.HandleAsync(context); + + var result = context.Result(); + + Assert.False(result.IsDuplicate); + } + [Fact] public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs index 0afcc7d67..0cea7973e 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs @@ -32,6 +32,26 @@ namespace Squidex.Infrastructure.Commands { } + protected override bool CanAcceptCreation(ICommand command) + { + if (command is CreateAuto update) + { + return update.Value != 99; + } + + return true; + } + + protected override bool CanAccept(ICommand command) + { + if (command is UpdateAuto update) + { + return update.Value != 99; + } + + return true; + } + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) @@ -92,7 +112,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); A.CallTo(() => persistence.ReadAsync(A._)) - .MustNotHaveHappened(); + .MustHaveHappened(); Assert.True(result is EntityCreatedResult); @@ -116,7 +136,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); A.CallTo(() => persistence.ReadAsync(A._)) - .MustNotHaveHappened(); + .MustHaveHappened(); Assert.True(result is EntitySavedResult); @@ -149,7 +169,15 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_only_load_once_on_update() + public async Task Should_load_on_create() + { + SetupEmpty(); + + await sut.ExecuteAsync(new CreateAuto()); + } + + [Fact] + public async Task Should_load_once_on_update() { SetupCreated(4); @@ -200,11 +228,11 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_not_throw_exception_when_already_created() + public async Task Should_throw_exception_when_already_created() { SetupCreated(4); - await sut.ExecuteAsync(new CreateAuto()); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); } [Fact] @@ -215,6 +243,22 @@ namespace Squidex.Infrastructure.Commands await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); } + [Fact] + public async Task Should_throw_exception_when_create_command_not_accepted() + { + SetupEmpty(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto { Value = 99 })); + } + + [Fact] + public async Task Should_throw_exception_when_update_command_not_accepted() + { + SetupCreated(4); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 })); + } + [Fact] public async Task Should_return_custom_result_on_create() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs index 4ab8f3138..95ae73b2a 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs @@ -34,6 +34,26 @@ namespace Squidex.Infrastructure.Commands { } + protected override bool CanAcceptCreation(ICommand command) + { + if (command is CreateAuto update) + { + return update.Value != 99; + } + + return true; + } + + protected override bool CanAccept(ICommand command) + { + if (command is UpdateAuto update) + { + return update.Value != 99; + } + + return true; + } + public override Task ExecuteAsync(IAggregateCommand command) { switch (command) @@ -145,7 +165,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); A.CallTo(() => persistence.ReadAsync(A._)) - .MustNotHaveHappened(); + .MustHaveHappened(); Assert.True(result is EntityCreatedResult); @@ -169,7 +189,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); A.CallTo(() => persistence.ReadAsync(A._)) - .MustNotHaveHappened(); + .MustHaveHappened(); Assert.True(result is EntitySavedResult); @@ -253,11 +273,11 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_not_throw_exception_when_already_created() + public async Task Should_throw_exception_when_already_created() { SetupCreated(4); - await sut.ExecuteAsync(new CreateAuto()); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); } [Fact] @@ -268,6 +288,22 @@ namespace Squidex.Infrastructure.Commands await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); } + [Fact] + public async Task Should_throw_exception_when_create_command_not_accepted() + { + SetupEmpty(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto { Value = 99 })); + } + + [Fact] + public async Task Should_throw_exception_when_update_command_not_accepted() + { + SetupCreated(4); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 })); + } + [Fact] public async Task Should_return_custom_result_on_create() { diff --git a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs index 4436aa274..6ea2d35a0 100644 --- a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs @@ -116,6 +116,19 @@ namespace Squidex.Web .MustNotHaveHappened(); } + [Fact] + public void Should_generate_409_for_DomainObjectConflictException() + { + var context = Error(new DomainObjectConflictException("1")); + + sut.OnException(context); + + Validate(409, context.Result, context.Exception); + + A.CallTo(() => log.Log(A._, A._, A._!)) + .MustNotHaveHappened(); + } + [Fact] public void Should_generate_412_for_DomainObjectVersionException() { diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs index 3d424e268..3317b88fc 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -29,6 +29,47 @@ namespace TestSuite.ApiTests _ = fixture; } + [Fact] + public async Task Should_upload_asset() + { + // STEP 1: Create asset + var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png"); + + using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var downloaded = await _.DownloadAsync(asset_1); + + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); + } + } + + [Fact] + public async Task Should_upload_asset_with_custom_id() + { + var id = Guid.NewGuid().ToString(); + + // STEP 1: Create asset + var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png", id: id); + + Assert.Equal(id, asset_1.Id); + } + + [Fact] + public async Task Should_not_create_asset_with_custom_id_twice() + { + var id = Guid.NewGuid().ToString(); + + // STEP 1: Create asset + await _.UploadFileAsync("Assets/logo-squared.png", "image/png", id: id); + + + // STEP 2: Create a new item with a custom id. + var ex = await Assert.ThrowsAsync(() => _.UploadFileAsync("Assets/logo-squared.png", "image/png", id: id)); + + Assert.Equal(409, ex.StatusCode); + } + [Fact] public async Task Should_replace_asset() { diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index 07977edf7..84fb118d2 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -8,6 +8,7 @@ using System; using System.Threading.Tasks; using Squidex.ClientLibrary; +using Squidex.ClientLibrary.Management; using TestSuite.Fixtures; using TestSuite.Model; using Xunit; @@ -27,14 +28,20 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_item_published_item() + public async Task Should_return_published_item() { TestEntity content = null; try { + // STEP 1: Create the item unpublished. content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); + + // STEP 2: Publish the item. await _.Contents.ChangeStatusAsync(content.Id, Status.Published); + + + // STEP 3: Retrieve the item. await _.Contents.GetAsync(content.Id); } finally @@ -52,10 +59,15 @@ namespace TestSuite.ApiTests TestEntity content = null; try { + // STEP 1: Create the item published. content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); + + // STEP 2: Archive the item. await _.Contents.ChangeStatusAsync(content.Id, Status.Archived); + + // STEP 3. Get a 404 for the item because it is not published anymore. await Assert.ThrowsAsync(() => _.Contents.GetAsync(content.Id)); } finally @@ -73,11 +85,16 @@ namespace TestSuite.ApiTests TestEntity content = null; try { + // STEP 1: Create the item unpublished. content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); + + // STEP 2: Change the status to publiushed and then to draft. await _.Contents.ChangeStatusAsync(content.Id, Status.Published); await _.Contents.ChangeStatusAsync(content.Id, Status.Draft); + + // STEP 3. Get a 404 for the item because it is not published anymore. await Assert.ThrowsAsync(() => _.Contents.GetAsync(content.Id)); } finally @@ -97,8 +114,11 @@ namespace TestSuite.ApiTests TestEntity content = null; try { + // STEP 1: Create a content item with a text that caused a bug before. content = await _.Contents.CreateAsync(new TestEntityData { String = text }, true); + + // STEP 2: Get the item and ensure that the text is the same. var updated = await _.Contents.GetAsync(content.Id); Assert.Equal(text, updated.Data.String); @@ -118,8 +138,11 @@ namespace TestSuite.ApiTests TestEntity content = null; try { + // STEP 1: Create the item unpublished. content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); + + // STEP 2. Get a 404 for the item because it is not published. await Assert.ThrowsAsync(() => _.Contents.GetAsync(content.Id)); } finally @@ -137,8 +160,11 @@ namespace TestSuite.ApiTests TestEntity content = null; try { + // STEP 1: Create the item published. content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); + + // STEP 2: Get the item. await _.Contents.GetAsync(content.Id); } finally @@ -153,14 +179,43 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_create_item_with_custom_id() { + var id = Guid.NewGuid().ToString(); + TestEntity content = null; try { - var id = Guid.NewGuid().ToString(); + // STEP 1: Create a new item with a custom id. + content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, id, true); + + Assert.Equal(id, content.Id); + } + finally + { + if (content != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_not_create_item_with_custom_id_twice() + { + var id = Guid.NewGuid().ToString(); + TestEntity content = null; + try + { + // STEP 1: Create a new item with a custom id. content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, id, true); Assert.Equal(id, content.Id); + + + // STEP 2: Create a new item with a custom id. + var ex = await Assert.ThrowsAsync(() => _.Contents.CreateAsync(new TestEntityData { Number = 1 }, id, true)); + + Assert.Contains("\"statusCode\":409", ex.Message); } finally { @@ -174,14 +229,20 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_create_item_with_custom_id_and_upsert() { + var id = Guid.NewGuid().ToString(); + TestEntity content = null; try { - var id = Guid.NewGuid().ToString(); - + // STEP 1: Upsert a new item with a custom id. content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 1 }, true); Assert.Equal(id, content.Id); + + + content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 2 }, true); + + Assert.Equal(1, content.Version); } finally { @@ -195,13 +256,16 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_upsert_item() { + var id = Guid.NewGuid().ToString(); + TestEntity content = null; try { - var id = Guid.NewGuid().ToString(); - + // STEP 1: Upsert a new item with a custom id. content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 1 }, true); + + // STEP 2: Upsert the item with a custom id and ensure that is has been updated. await _.Contents.UpsertAsync(id, new TestEntityData { Number = 2 }); var updated = await _.Contents.GetAsync(content.Id); @@ -223,8 +287,11 @@ namespace TestSuite.ApiTests TestEntity content = null; try { + // STEP 1: Create a new item. content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, true); + + // STEP 2: Update the item and ensure that the data has changed. await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number = 2 }); var updated = await _.Contents.GetAsync(content.Id); @@ -246,8 +313,11 @@ namespace TestSuite.ApiTests TestEntity content = null; try { + // STEP 1: Create a new item. content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); + + // STEP 2: Update the item and ensure that the data has changed. await _.Contents.PatchAsync(content.Id, new TestEntityData { Number = 2 }); var updated = await _.Contents.GetAsync(content.Id); @@ -266,10 +336,15 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_delete_item() { + // STEP 1: Create a new item. var content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, true); + + // STEP 2: Delete the item. await _.Contents.DeleteAsync(content.Id); + + // STEP 3: Retrieve all items and ensure that the deleted item does not exist. var updated = await _.Contents.GetAsync(); Assert.DoesNotContain(updated.Items, x => x.Id == content.Id); diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs index 10934a882..09631b3ee 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs @@ -50,7 +50,7 @@ namespace TestSuite.Fixtures } } - public async Task UploadFileAsync(string path, string mimeType, string fileName = null, string parentId = null) + public async Task UploadFileAsync(string path, string mimeType, string fileName = null, string parentId = null, string id = null) { var fileInfo = new FileInfo(path); @@ -58,7 +58,7 @@ namespace TestSuite.Fixtures { var upload = new FileParameter(stream, fileName ?? RandomName(fileInfo), mimeType); - return await Assets.PostAssetAsync(AppName, parentId?.ToString(), upload); + return await Assets.PostAssetAsync(AppName, parentId, id, true, upload); } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index 0fe0df39d..7411085cc 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -1,6 +1,7 @@  netcoreapp3.1 + TestSuite @@ -9,15 +10,16 @@ - ..\..\..\Squidex.ruleset - TestSuite + + + diff --git a/backend/tools/TestSuite/TestSuite.sln b/backend/tools/TestSuite/TestSuite.sln index 8bc1f5817..9a7921248 100644 --- a/backend/tools/TestSuite/TestSuite.sln +++ b/backend/tools/TestSuite/TestSuite.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.ApiTests", "TestS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.LoadTests", "TestSuite.LoadTests\TestSuite.LoadTests.csproj", "{F37572D9-4880-40F4-B3CB-83F58A40CA48}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.ClientLibrary", "..\..\..\..\squidex-samples\csharp\Squidex.ClientLibrary\Squidex.ClientLibrary\Squidex.ClientLibrary.csproj", "{880BC21F-49C6-4DF6-B668-D4FB0DAF85A9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {F37572D9-4880-40F4-B3CB-83F58A40CA48}.Debug|Any CPU.Build.0 = Debug|Any CPU {F37572D9-4880-40F4-B3CB-83F58A40CA48}.Release|Any CPU.ActiveCfg = Release|Any CPU {F37572D9-4880-40F4-B3CB-83F58A40CA48}.Release|Any CPU.Build.0 = Release|Any CPU + {880BC21F-49C6-4DF6-B668-D4FB0DAF85A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {880BC21F-49C6-4DF6-B668-D4FB0DAF85A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {880BC21F-49C6-4DF6-B668-D4FB0DAF85A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {880BC21F-49C6-4DF6-B668-D4FB0DAF85A9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE