Browse Source

More tests around custom ids.

pull/590/head
Sebastian 6 years ago
parent
commit
f96dfb0ddd
  1. 1
      backend/i18n/source/backend_en.json
  2. 19
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  3. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  4. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs
  5. 9
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs
  6. 14
      backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs
  7. 32
      backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs
  8. 3
      backend/src/Squidex.Shared/Texts.it.resx
  9. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  10. 3
      backend/src/Squidex.Shared/Texts.resx
  11. 3
      backend/src/Squidex.Web/ApiExceptionConverter.cs
  12. 3
      backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs
  13. 11
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  14. 15
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  15. 54
      backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs
  16. 44
      backend/tests/Squidex.Infrastructure.Tests/Commands/LogSnapshotDomainObjectTests.cs
  17. 13
      backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs
  18. 41
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  19. 87
      backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs
  20. 4
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs
  21. 6
      backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
  22. 6
      backend/tools/TestSuite/TestSuite.sln

1
backend/i18n/source/backend_en.json

@ -200,6 +200,7 @@
"dotnet_identity_UserLockedOut": "User is locked out.", "dotnet_identity_UserLockedOut": "User is locked out.",
"exception.invalidJsonQuery": "Json query not valid: {message}", "exception.invalidJsonQuery": "Json query not valid: {message}",
"exception.invalidJsonQueryJson": "Json query not valid json: {message}", "exception.invalidJsonQueryJson": "Json query not valid json: {message}",
"exceptions.domainObjectConflict": "Entity ({id}) already exists.",
"exceptions.domainObjectDeleted": "Entity ({id}) has been deleted.", "exceptions.domainObjectDeleted": "Entity ({id}) has been deleted.",
"exceptions.domainObjectNotFound": "Entity ({id}) does not exist.", "exceptions.domainObjectNotFound": "Entity ({id}) does not exist.",
"exceptions.domainObjectVersion": "Entity ({id}) requested version {expectedVersion}, but found {currentVersion}.", "exceptions.domainObjectVersion": "Entity ({id}) requested version {expectedVersion}, but found {currentVersion}.",

19
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 ctx = contextProvider.Context.Clone().WithoutAssetEnrichment();
var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); if (!createAsset.Duplicate)
foreach (var existing in existings)
{ {
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); await next(context);
return; return;
}
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
public HashSet<string> Tags { get; } = new HashSet<string>(); public HashSet<string> Tags { get; } = new HashSet<string>();
public bool Duplicate { get; set; }
public CreateAsset() public CreateAsset()
{ {
AssetId = DomainId.NewGuid(); AssetId = DomainId.NewGuid();

1
backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs

@ -9,7 +9,6 @@ using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow; using System.Threading.Tasks.Dataflow;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;

9
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using MongoDB.Driver.Core.Clusters;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.EventSourcing namespace Squidex.Infrastructure.EventSourcing
@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.EventSourcing
get { return Collection; } get { return Collection; }
} }
public bool IsReplicaSet { get; } public bool IsReplicaSet { get; private set; }
public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) public MongoEventStore(IMongoDatabase database, IEventNotifier notifier)
: base(database) : base(database)
@ -51,9 +52,9 @@ namespace Squidex.Infrastructure.EventSourcing
return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority };
} }
protected override Task SetupCollectionAsync(IMongoCollection<MongoEventCommit> collection, CancellationToken ct = default) protected override async Task SetupCollectionAsync(IMongoCollection<MongoEventCommit> collection, CancellationToken ct = default)
{ {
return collection.Indexes.CreateManyAsync(new[] await collection.Indexes.CreateManyAsync(new[]
{ {
new CreateIndexModel<MongoEventCommit>( new CreateIndexModel<MongoEventCommit>(
Index Index
@ -75,6 +76,8 @@ namespace Squidex.Infrastructure.EventSourcing
Unique = true Unique = true
}) })
}, ct); }, ct);
IsReplicaSet = Database.Client.Cluster.Description.Type == ClusterType.ReplicaSet;
} }
} }
} }

14
backend/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs

@ -146,10 +146,7 @@ namespace Squidex.Infrastructure.Commands
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
Guard.NotNull(handler, nameof(handler)); Guard.NotNull(handler, nameof(handler));
if (isUpdate) await EnsureLoadedAsync();
{
await EnsureLoadedAsync();
}
if (IsDeleted()) if (IsDeleted())
{ {
@ -160,14 +157,19 @@ namespace Squidex.Infrastructure.Commands
{ {
if (!CanAccept(command)) if (!CanAccept(command))
{ {
throw new NotSupportedException("Invalid command."); throw new DomainException("Invalid command.");
} }
} }
else else
{ {
if (Version > EtagVersion.Empty)
{
throw new DomainObjectConflictException(uniqueId.ToString());
}
if (!CanAcceptCreation(command)) if (!CanAcceptCreation(command))
{ {
throw new NotSupportedException("Invalid command."); throw new DomainException("Invalid command.");
} }
} }

32
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 });
}
}
}

3
backend/src/Squidex.Shared/Texts.it.resx

@ -685,6 +685,9 @@
<data name="exception.invalidJsonQueryJson" xml:space="preserve"> <data name="exception.invalidJsonQueryJson" xml:space="preserve">
<value>La query Json non è valida: {message}</value> <value>La query Json non è valida: {message}</value>
</data> </data>
<data name="exceptions.domainObjectConflict" xml:space="preserve">
<value>Entity ({id}) already exists.</value>
</data>
<data name="exceptions.domainObjectDeleted" xml:space="preserve"> <data name="exceptions.domainObjectDeleted" xml:space="preserve">
<value>L'entità ({id}) è stata cancellata.</value> <value>L'entità ({id}) è stata cancellata.</value>
</data> </data>

3
backend/src/Squidex.Shared/Texts.nl.resx

@ -685,6 +685,9 @@
<data name="exception.invalidJsonQueryJson" xml:space="preserve"> <data name="exception.invalidJsonQueryJson" xml:space="preserve">
<value>Json-query is niet geldig json: {message}</value> <value>Json-query is niet geldig json: {message}</value>
</data> </data>
<data name="exceptions.domainObjectConflict" xml:space="preserve">
<value>Entity ({id}) already exists.</value>
</data>
<data name="exceptions.domainObjectDeleted" xml:space="preserve"> <data name="exceptions.domainObjectDeleted" xml:space="preserve">
<value>Entiteit ({id}) is verwijderd.</value> <value>Entiteit ({id}) is verwijderd.</value>
</data> </data>

3
backend/src/Squidex.Shared/Texts.resx

@ -685,6 +685,9 @@
<data name="exception.invalidJsonQueryJson" xml:space="preserve"> <data name="exception.invalidJsonQueryJson" xml:space="preserve">
<value>Json query not valid json: {message}</value> <value>Json query not valid json: {message}</value>
</data> </data>
<data name="exceptions.domainObjectConflict" xml:space="preserve">
<value>Entity ({id}) already exists.</value>
</data>
<data name="exceptions.domainObjectDeleted" xml:space="preserve"> <data name="exceptions.domainObjectDeleted" xml:space="preserve">
<value>Entity ({id}) has been deleted.</value> <value>Entity ({id}) has been deleted.</value>
</data> </data>

3
backend/src/Squidex.Web/ApiExceptionConverter.cs

@ -91,6 +91,9 @@ namespace Squidex.Web
case DomainObjectVersionException _: case DomainObjectVersionException _:
return (CreateError(412, exception.Message), true); return (CreateError(412, exception.Message), true);
case DomainObjectConflictException _:
return (CreateError(409, exception.Message), true);
case DomainForbiddenException _: case DomainForbiddenException _:
return (CreateError(403, exception.Message), true); return (CreateError(403, exception.Message), true);

3
backend/src/Squidex.Web/ApiExceptionFilterAttribute.cs

@ -35,7 +35,8 @@ namespace Squidex.Web
{ {
var log = context.HttpContext.RequestServices.GetService<ISemanticLog>(); var log = context.HttpContext.RequestServices.GetService<ISemanticLog>();
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); context.Result = GetResult(error);

11
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -172,6 +172,8 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="parentId">The optional parent folder id.</param> /// <param name="parentId">The optional parent folder id.</param>
/// <param name="file">The file to upload.</param> /// <param name="file">The file to upload.</param>
/// <param name="id">The optional custom asset id.</param>
/// <param name="duplicate">True to duplicate the asset, event if the file has been uploaded.</param>
/// <returns> /// <returns>
/// 201 => Asset created. /// 201 => Asset created.
/// 404 => App not found. /// 404 => App not found.
@ -186,11 +188,16 @@ namespace Squidex.Areas.Api.Controllers.Assets
[AssetRequestSizeLimit] [AssetRequestSizeLimit]
[ApiPermissionOrAnonymous(Permissions.AppAssetsCreate)] [ApiPermissionOrAnonymous(Permissions.AppAssetsCreate)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> PostAsset(string app, [FromQuery] string parentId, IFormFile file) public async Task<IActionResult> PostAsset(string app, [FromQuery] string parentId, IFormFile file, [FromQuery] string? id = null, [FromQuery] bool duplicate = false)
{ {
var assetFile = await CheckAssetFileAsync(file); 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); var response = await InvokeCommandAsync(command);

15
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -175,6 +175,21 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.True(result.IsDuplicate); 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<AssetCreatedResult>();
Assert.False(result.IsDuplicate);
}
[Fact] [Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found() public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found()
{ {

54
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<object?> ExecuteAsync(IAggregateCommand command) public override Task<object?> ExecuteAsync(IAggregateCommand command)
{ {
switch (command) switch (command)
@ -92,7 +112,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1))) A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1)))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => persistence.ReadAsync(A<long>._)) A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustNotHaveHappened(); .MustHaveHappened();
Assert.True(result is EntityCreatedResult<DomainId>); Assert.True(result is EntityCreatedResult<DomainId>);
@ -116,7 +136,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1))) A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1)))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => persistence.ReadAsync(A<long>._)) A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustNotHaveHappened(); .MustHaveHappened();
Assert.True(result is EntitySavedResult); Assert.True(result is EntitySavedResult);
@ -149,7 +169,15 @@ namespace Squidex.Infrastructure.Commands
} }
[Fact] [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); SetupCreated(4);
@ -200,11 +228,11 @@ namespace Squidex.Infrastructure.Commands
} }
[Fact] [Fact]
public async Task Should_not_throw_exception_when_already_created() public async Task Should_throw_exception_when_already_created()
{ {
SetupCreated(4); SetupCreated(4);
await sut.ExecuteAsync(new CreateAuto()); await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto()));
} }
[Fact] [Fact]
@ -215,6 +243,22 @@ namespace Squidex.Infrastructure.Commands
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(new UpdateAuto())); await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(new UpdateAuto()));
} }
[Fact]
public async Task Should_throw_exception_when_create_command_not_accepted()
{
SetupEmpty();
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new CreateAuto { Value = 99 }));
}
[Fact]
public async Task Should_throw_exception_when_update_command_not_accepted()
{
SetupCreated(4);
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 }));
}
[Fact] [Fact]
public async Task Should_return_custom_result_on_create() public async Task Should_return_custom_result_on_create()
{ {

44
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<object?> ExecuteAsync(IAggregateCommand command) public override Task<object?> ExecuteAsync(IAggregateCommand command)
{ {
switch (command) switch (command)
@ -145,7 +165,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1))) A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1)))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => persistence.ReadAsync(A<long>._)) A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustNotHaveHappened(); .MustHaveHappened();
Assert.True(result is EntityCreatedResult<DomainId>); Assert.True(result is EntityCreatedResult<DomainId>);
@ -169,7 +189,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1))) A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1)))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => persistence.ReadAsync(A<long>._)) A.CallTo(() => persistence.ReadAsync(A<long>._))
.MustNotHaveHappened(); .MustHaveHappened();
Assert.True(result is EntitySavedResult); Assert.True(result is EntitySavedResult);
@ -253,11 +273,11 @@ namespace Squidex.Infrastructure.Commands
} }
[Fact] [Fact]
public async Task Should_not_throw_exception_when_already_created() public async Task Should_throw_exception_when_already_created()
{ {
SetupCreated(4); SetupCreated(4);
await sut.ExecuteAsync(new CreateAuto()); await Assert.ThrowsAsync<DomainObjectConflictException>(() => sut.ExecuteAsync(new CreateAuto()));
} }
[Fact] [Fact]
@ -268,6 +288,22 @@ namespace Squidex.Infrastructure.Commands
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(new UpdateAuto())); await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(new UpdateAuto()));
} }
[Fact]
public async Task Should_throw_exception_when_create_command_not_accepted()
{
SetupEmpty();
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new CreateAuto { Value = 99 }));
}
[Fact]
public async Task Should_throw_exception_when_update_command_not_accepted()
{
SetupCreated(4);
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 }));
}
[Fact] [Fact]
public async Task Should_return_custom_result_on_create() public async Task Should_return_custom_result_on_create()
{ {

13
backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs

@ -116,6 +116,19 @@ namespace Squidex.Web
.MustNotHaveHappened(); .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<SemanticLogLevel>._, A<Exception?>._, A<LogFormatter>._!))
.MustNotHaveHappened();
}
[Fact] [Fact]
public void Should_generate_412_for_DomainObjectVersionException() public void Should_generate_412_for_DomainObjectVersionException()
{ {

41
backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -29,6 +29,47 @@ namespace TestSuite.ApiTests
_ = fixture; _ = 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<SquidexManagementException>(() => _.UploadFileAsync("Assets/logo-squared.png", "image/png", id: id));
Assert.Equal(409, ex.StatusCode);
}
[Fact] [Fact]
public async Task Should_replace_asset() public async Task Should_replace_asset()
{ {

87
backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.ClientLibrary; using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures; using TestSuite.Fixtures;
using TestSuite.Model; using TestSuite.Model;
using Xunit; using Xunit;
@ -27,14 +28,20 @@ namespace TestSuite.ApiTests
} }
[Fact] [Fact]
public async Task Should_return_item_published_item() public async Task Should_return_published_item()
{ {
TestEntity content = null; TestEntity content = null;
try try
{ {
// STEP 1: Create the item unpublished.
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
// STEP 2: Publish the item.
await _.Contents.ChangeStatusAsync(content.Id, Status.Published); await _.Contents.ChangeStatusAsync(content.Id, Status.Published);
// STEP 3: Retrieve the item.
await _.Contents.GetAsync(content.Id); await _.Contents.GetAsync(content.Id);
} }
finally finally
@ -52,10 +59,15 @@ namespace TestSuite.ApiTests
TestEntity content = null; TestEntity content = null;
try try
{ {
// STEP 1: Create the item published.
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true);
// STEP 2: Archive the item.
await _.Contents.ChangeStatusAsync(content.Id, Status.Archived); await _.Contents.ChangeStatusAsync(content.Id, Status.Archived);
// STEP 3. Get a 404 for the item because it is not published anymore.
await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id)); await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id));
} }
finally finally
@ -73,11 +85,16 @@ namespace TestSuite.ApiTests
TestEntity content = null; TestEntity content = null;
try try
{ {
// STEP 1: Create the item unpublished.
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); 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.Published);
await _.Contents.ChangeStatusAsync(content.Id, Status.Draft); await _.Contents.ChangeStatusAsync(content.Id, Status.Draft);
// STEP 3. Get a 404 for the item because it is not published anymore.
await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id)); await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id));
} }
finally finally
@ -97,8 +114,11 @@ namespace TestSuite.ApiTests
TestEntity content = null; TestEntity content = null;
try try
{ {
// STEP 1: Create a content item with a text that caused a bug before.
content = await _.Contents.CreateAsync(new TestEntityData { String = text }, true); 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); var updated = await _.Contents.GetAsync(content.Id);
Assert.Equal(text, updated.Data.String); Assert.Equal(text, updated.Data.String);
@ -118,8 +138,11 @@ namespace TestSuite.ApiTests
TestEntity content = null; TestEntity content = null;
try try
{ {
// STEP 1: Create the item unpublished.
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
// STEP 2. Get a 404 for the item because it is not published.
await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id)); await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id));
} }
finally finally
@ -137,8 +160,11 @@ namespace TestSuite.ApiTests
TestEntity content = null; TestEntity content = null;
try try
{ {
// STEP 1: Create the item published.
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true);
// STEP 2: Get the item.
await _.Contents.GetAsync(content.Id); await _.Contents.GetAsync(content.Id);
} }
finally finally
@ -153,14 +179,43 @@ namespace TestSuite.ApiTests
[Fact] [Fact]
public async Task Should_create_item_with_custom_id() public async Task Should_create_item_with_custom_id()
{ {
var id = Guid.NewGuid().ToString();
TestEntity content = null; TestEntity content = null;
try 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); content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, id, true);
Assert.Equal(id, content.Id); Assert.Equal(id, content.Id);
// STEP 2: Create a new item with a custom id.
var ex = await Assert.ThrowsAsync<SquidexException>(() => _.Contents.CreateAsync(new TestEntityData { Number = 1 }, id, true));
Assert.Contains("\"statusCode\":409", ex.Message);
} }
finally finally
{ {
@ -174,14 +229,20 @@ namespace TestSuite.ApiTests
[Fact] [Fact]
public async Task Should_create_item_with_custom_id_and_upsert() public async Task Should_create_item_with_custom_id_and_upsert()
{ {
var id = Guid.NewGuid().ToString();
TestEntity content = null; TestEntity content = null;
try 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); content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 1 }, true);
Assert.Equal(id, content.Id); Assert.Equal(id, content.Id);
content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 2 }, true);
Assert.Equal(1, content.Version);
} }
finally finally
{ {
@ -195,13 +256,16 @@ namespace TestSuite.ApiTests
[Fact] [Fact]
public async Task Should_upsert_item() public async Task Should_upsert_item()
{ {
var id = Guid.NewGuid().ToString();
TestEntity content = null; TestEntity content = null;
try 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); 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 }); await _.Contents.UpsertAsync(id, new TestEntityData { Number = 2 });
var updated = await _.Contents.GetAsync(content.Id); var updated = await _.Contents.GetAsync(content.Id);
@ -223,8 +287,11 @@ namespace TestSuite.ApiTests
TestEntity content = null; TestEntity content = null;
try try
{ {
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, true); 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 }); await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number = 2 });
var updated = await _.Contents.GetAsync(content.Id); var updated = await _.Contents.GetAsync(content.Id);
@ -246,8 +313,11 @@ namespace TestSuite.ApiTests
TestEntity content = null; TestEntity content = null;
try try
{ {
// STEP 1: Create a new item.
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); 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 }); await _.Contents.PatchAsync(content.Id, new TestEntityData { Number = 2 });
var updated = await _.Contents.GetAsync(content.Id); var updated = await _.Contents.GetAsync(content.Id);
@ -266,10 +336,15 @@ namespace TestSuite.ApiTests
[Fact] [Fact]
public async Task Should_delete_item() public async Task Should_delete_item()
{ {
// STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, true); var content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, true);
// STEP 2: Delete the item.
await _.Contents.DeleteAsync(content.Id); await _.Contents.DeleteAsync(content.Id);
// STEP 3: Retrieve all items and ensure that the deleted item does not exist.
var updated = await _.Contents.GetAsync(); var updated = await _.Contents.GetAsync();
Assert.DoesNotContain(updated.Items, x => x.Id == content.Id); Assert.DoesNotContain(updated.Items, x => x.Id == content.Id);

4
backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs

@ -50,7 +50,7 @@ namespace TestSuite.Fixtures
} }
} }
public async Task<AssetDto> UploadFileAsync(string path, string mimeType, string fileName = null, string parentId = null) public async Task<AssetDto> UploadFileAsync(string path, string mimeType, string fileName = null, string parentId = null, string id = null)
{ {
var fileInfo = new FileInfo(path); var fileInfo = new FileInfo(path);
@ -58,7 +58,7 @@ namespace TestSuite.Fixtures
{ {
var upload = new FileParameter(stream, fileName ?? RandomName(fileInfo), mimeType); 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);
} }
} }

6
backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>TestSuite</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fody" Version="6.1.1"> <PackageReference Include="Fody" Version="6.1.1">
@ -9,15 +10,16 @@
</PackageReference> </PackageReference>
<PackageReference Include="Lazy.Fody" Version="1.8.0" PrivateAssets="all" /> <PackageReference Include="Lazy.Fody" Version="1.8.0" PrivateAssets="all" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="5.5.0-beta3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>..\..\..\Squidex.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>..\..\..\Squidex.ruleset</CodeAnalysisRuleSet>
<RootNamespace>TestSuite</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="..\..\..\stylecop.json" Link="stylecop.json" /> <AdditionalFiles Include="..\..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\squidex-samples\csharp\Squidex.ClientLibrary\Squidex.ClientLibrary\Squidex.ClientLibrary.csproj" />
</ItemGroup>
</Project> </Project>

6
backend/tools/TestSuite/TestSuite.sln

@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.ApiTests", "TestS
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.LoadTests", "TestSuite.LoadTests\TestSuite.LoadTests.csproj", "{F37572D9-4880-40F4-B3CB-83F58A40CA48}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.LoadTests", "TestSuite.LoadTests\TestSuite.LoadTests.csproj", "{F37572D9-4880-40F4-B3CB-83F58A40CA48}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{F37572D9-4880-40F4-B3CB-83F58A40CA48}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

Loading…
Cancel
Save