diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index b38838e99..d66b14a94 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs @@ -26,14 +26,19 @@ namespace Squidex.Domain.Apps.Entities.Assets { FileName = command.File.FileName, FileSize = command.File.FileSize, - FileVersion = State.FileVersion + 1, + FileVersion = 0, MimeType = command.File.MimeType, PixelWidth = command.ImageInfo?.PixelWidth, PixelHeight = command.ImageInfo?.PixelHeight, IsImage = command.ImageInfo != null }); - UpdateState(command, s => SimpleMapper.Map(@event, s)); + UpdateState(command, s => + { + s.TotalSize = @event.FileSize; + + SimpleMapper.Map(@event, s); + }); RaiseEvent(@event); @@ -54,7 +59,12 @@ namespace Squidex.Domain.Apps.Entities.Assets IsImage = command.ImageInfo != null }); - UpdateState(command, s => SimpleMapper.Map(@event, s)); + UpdateState(command, s => + { + s.TotalSize += @event.FileSize; + + SimpleMapper.Map(@event, s); + }); RaiseEvent(@event); diff --git a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index c13e058d7..3775b6244 100644 --- a/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -14,6 +14,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities { public abstract class DomainObjectState : Cloneable, + IUpdateableEntity, IUpdateableEntityWithCreatedBy, IUpdateableEntityWithLastModifiedBy, IUpdateableEntityWithVersion diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs index d100f596a..b7fbf2671 100644 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -8,6 +8,7 @@ using System; using NodaTime; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities { @@ -17,28 +18,24 @@ namespace Squidex.Domain.Apps.Entities { var timestamp = SystemClock.Instance.GetCurrentInstant(); + SetId(entity, command); SetAppId(entity, command); - SetVersion(entity); SetCreated(entity, timestamp); SetCreatedBy(entity, command); SetLastModified(entity, timestamp); SetLastModifiedBy(entity, command); + SetVersion(entity); updater?.Invoke(entity); return entity; } - private static void SetLastModified(IEntity entity, Instant timestamp) - { - entity.LastModified = timestamp; - } - - private static void SetCreated(IEntity entity, Instant timestamp) + private static void SetId(IEntity entity, SquidexCommand command) { - if (entity.Created == default(Instant)) + if (entity is IUpdateableEntity updateable && command is IAggregateCommand aggregateCommand) { - entity.Created = timestamp; + updateable.Id = aggregateCommand.AggregateId; } } @@ -50,6 +47,14 @@ namespace Squidex.Domain.Apps.Entities } } + private static void SetCreated(IEntity entity, Instant timestamp) + { + if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant)) + { + updateable.Created = timestamp; + } + } + private static void SetCreatedBy(IEntity entity, SquidexCommand command) { if (entity is IUpdateableEntityWithCreatedBy withCreatedBy && withCreatedBy.CreatedBy == null) @@ -58,6 +63,14 @@ namespace Squidex.Domain.Apps.Entities } } + private static void SetLastModified(IEntity entity, Instant timestamp) + { + if (entity is IUpdateableEntity updateable) + { + updateable.LastModified = timestamp; + } + } + private static void SetLastModifiedBy(IEntity entity, SquidexCommand command) { if (entity is IUpdateableEntityWithLastModifiedBy withModifiedBy) diff --git a/src/Squidex.Domain.Apps.Entities/IEntity.cs b/src/Squidex.Domain.Apps.Entities/IEntity.cs index 9aa8ea2b0..a23a78d08 100644 --- a/src/Squidex.Domain.Apps.Entities/IEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/IEntity.cs @@ -13,10 +13,10 @@ namespace Squidex.Domain.Apps.Entities { public interface IEntity { - Guid Id { get; set; } + Guid Id { get; } - Instant Created { get; set; } + Instant Created { get; } - Instant LastModified { get; set; } + Instant LastModified { get; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs b/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs new file mode 100644 index 000000000..7e1b2f03d --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IUpdateableEntity.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// IUpdateableEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IUpdateableEntity + { + Guid Id { get; set; } + + Instant Created { get; set; } + + Instant LastModified { get; set; } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 0d14e580e..6dd770faf 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -200,7 +200,7 @@ namespace Squidex.Infrastructure.EventSourcing { var document = await Collection.Find(Filter.Eq(EventStreamField, streamName)) - .Project(Project + .Project(Projection .Include(EventStreamOffsetField) .Include(EventsCountField)) .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 929757756..056dd8595 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -13,18 +13,21 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.Tasks; +#pragma warning disable RECS0108 // Warns about static fields in generic types + namespace Squidex.Infrastructure.MongoDb { public abstract class MongoRepositoryBase : IExternalSystem { private const string CollectionFormat = "{0}Set"; + protected static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; protected static readonly SortDefinitionBuilder Sort = Builders.Sort; protected static readonly UpdateDefinitionBuilder Update = Builders.Update; protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; - protected static readonly ProjectionDefinitionBuilder Project = Builders.Projection; + protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; private readonly IMongoDatabase mongoDatabase; private Lazy> mongoCollection; diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index c9d728434..0eaa6c56e 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -6,46 +6,34 @@ // All rights reserved. // ========================================================================== -using System; using System.Threading.Tasks; using MongoDB.Driver; using Newtonsoft.Json; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { - public sealed class MongoSnapshotStore : ISnapshotStore, IExternalSystem + public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore, IExternalSystem { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; - private readonly IMongoDatabase database; private readonly JsonSerializer serializer; public MongoSnapshotStore(IMongoDatabase database, JsonSerializer serializer) + : base(database) { - Guard.NotNull(database, nameof(database)); Guard.NotNull(serializer, nameof(serializer)); - this.database = database; this.serializer = serializer; } - public void Connect() + protected override string CollectionName() { - try - { - database.ListCollections(); - } - catch (Exception ex) - { - throw new ConfigurationException($"MongoDb connection failed to connect to database {database.DatabaseNamespace.DatabaseName}", ex); - } + return $"States_{typeof(T).Name}"; } - public async Task<(T Value, long Version)> ReadAsync(string key) + public async Task<(T Value, long Version)> ReadAsync(string key) { - var collection = GetCollection(); - var existing = - await collection.Find(x => x.Id == key) + await Collection.Find(x => x.Id == key) .FirstOrDefaultAsync(); if (existing != null) @@ -56,18 +44,16 @@ namespace Squidex.Infrastructure.States return (default(T), -1); } - public async Task WriteAsync(string key, T value, long oldVersion, long newVersion) + public async Task WriteAsync(string key, T value, long oldVersion, long newVersion) { - var collection = GetCollection(); - try { - await collection.UpdateOneAsync( - Builders>.Filter.And( - Builders>.Filter.Eq(x => x.Id, key), - Builders>.Filter.Eq(x => x.Version, oldVersion) + await Collection.UpdateOneAsync( + Filter.And( + Filter.Eq(x => x.Id, key), + Filter.Eq(x => x.Version, oldVersion) ), - Builders>.Update + Update .Set(x => x.Doc, value) .Set(x => x.Version, newVersion), Upsert); @@ -77,8 +63,8 @@ namespace Squidex.Infrastructure.States if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = - await collection.Find(x => x.Id == key) - .Project>(Builders>.Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); + await Collection.Find(x => x.Id == key) + .Project>(Projection.Exclude(x => x.Id)).FirstOrDefaultAsync(); if (existingVersion != null) { @@ -91,10 +77,5 @@ namespace Squidex.Infrastructure.States } } } - - private IMongoCollection> GetCollection() - { - return database.GetCollection>($"States_{typeof(T).Name}"); - } } } diff --git a/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/src/Squidex.Infrastructure/States/ISnapshotStore.cs index 8b43eed65..38d62d737 100644 --- a/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -10,10 +10,10 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.States { - public interface ISnapshotStore + public interface ISnapshotStore { - Task WriteAsync(string key, T value, long oldVersion, long newVersion); + Task WriteAsync(string key, T value, long oldVersion, long newVersion); - Task<(T Value, long Version)> ReadAsync(string key); + Task<(T Value, long Version)> ReadAsync(string key); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index b4a517616..d127964ea 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -13,10 +13,10 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - public sealed class Persistence : IPersistence + internal sealed class Persistence : IPersistence { private readonly string ownerKey; - private readonly ISnapshotStore snapshotStore; + private readonly ISnapshotStore snapshotStore; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; @@ -30,13 +30,11 @@ namespace Squidex.Infrastructure.States Action invalidate, IEventStore eventStore, IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, + ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, Func applyState, Func, Task> applyEvent) { - Guard.NotNull(ownerKey, nameof(ownerKey)); - this.ownerKey = ownerKey; this.applyState = applyState; this.applyEvent = applyEvent; @@ -54,7 +52,7 @@ namespace Squidex.Infrastructure.States if (snapshotStore != null) { - var (state, position) = await snapshotStore.ReadAsync(ownerKey); + var (state, position) = await snapshotStore.ReadAsync(ownerKey); positionSnapshot = position; positionEvent = position; @@ -104,13 +102,10 @@ namespace Squidex.Infrastructure.States public async Task WriteSnapshotAsync(TState state) { - if (snapshotStore == null) - { - throw new InvalidOperationException("Snapshots are not supported."); - } - var newPosition = - eventStore != null ? positionEvent : positionSnapshot + 1; + eventStore != null ? + positionEvent : + positionSnapshot + 1; if (newPosition != positionSnapshot) { @@ -126,18 +121,13 @@ namespace Squidex.Infrastructure.States positionSnapshot = newPosition; } - invalidate(); + invalidate?.Invoke(); } public async Task WriteEventsAsync(params Envelope[] @events) { Guard.NotNull(events, nameof(@events)); - if (eventStore == null) - { - throw new InvalidOperationException("Events are not supported."); - } - if (@events.Length > 0) { var commitId = Guid.NewGuid(); @@ -157,7 +147,7 @@ namespace Squidex.Infrastructure.States positionEvent += events.Length; } - invalidate(); + invalidate?.Invoke(); } private EventData[] GetEventData(Envelope[] events, Guid commitId) diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index a0513c441..4394b8f06 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -21,7 +21,6 @@ namespace Squidex.Infrastructure.States private readonly IPubSub pubSub; private readonly IMemoryCache statesCache; private readonly IServiceProvider services; - private readonly ISnapshotStore snapshotStore; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; @@ -54,22 +53,19 @@ namespace Squidex.Infrastructure.States IEventStore eventStore, IEventDataFormatter eventDataFormatter, IServiceProvider services, - ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver) { Guard.NotNull(services, nameof(services)); Guard.NotNull(eventStore, nameof(eventStore)); Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); Guard.NotNull(pubSub, nameof(pubSub)); - Guard.NotNull(snapshotStore, nameof(snapshotStore)); Guard.NotNull(statesCache, nameof(statesCache)); Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); - this.services = services; this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; this.pubSub = pubSub; - this.snapshotStore = snapshotStore; + this.services = services; this.statesCache = statesCache; this.streamNameResolver = streamNameResolver; } @@ -89,7 +85,7 @@ namespace Squidex.Infrastructure.States { Guard.NotNull(key, nameof(key)); - var stateStore = new Store(() => { }, eventStore, eventDataFormatter, snapshotStore, streamNameResolver); + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver); var state = (T)services.GetService(typeof(T)); await state.ActivateAsync(key, stateStore); @@ -110,10 +106,10 @@ namespace Squidex.Infrastructure.States var state = (T)services.GetService(typeof(T)); - var stateStore = new Store(() => + var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, () => { pubSub.Publish(new InvalidateMessage { Key = key }, false); - }, eventStore, eventDataFormatter, snapshotStore, streamNameResolver); + }); stateObj = new ObjectHolder(state, key, stateStore); diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index 0fd3faab9..b217dac38 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -15,37 +15,46 @@ namespace Squidex.Infrastructure.States public sealed class Store : IStore { private readonly Action invalidate; - private readonly ISnapshotStore snapshotStore; + private readonly IServiceProvider services; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; private readonly IEventDataFormatter eventDataFormatter; public Store( - Action invalidate, IEventStore eventStore, IEventDataFormatter eventDataFormatter, - ISnapshotStore snapshotStore, - IStreamNameResolver streamNameResolver) + IServiceProvider services, + IStreamNameResolver streamNameResolver, + Action invalidate = null) { this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; this.invalidate = invalidate; - this.snapshotStore = snapshotStore; + this.services = services; this.streamNameResolver = streamNameResolver; } public IPersistence WithEventSourcing(string key, Func, Task> applyEvent) { - return new Persistence(key, invalidate, eventStore, eventDataFormatter, null, streamNameResolver, null, applyEvent); + return CreatePersistence(key, null, applyEvent); } public IPersistence WithSnapshots(string key, Func applySnapshot) { - return new Persistence(key, invalidate, null, null, snapshotStore, null, applySnapshot, null); + return CreatePersistence(key, applySnapshot, null); } public IPersistence WithSnapshotsAndEventSourcing(string key, Func applySnapshot, Func, Task> applyEvent) { + return CreatePersistence(key, applySnapshot, applyEvent); + } + + private IPersistence CreatePersistence(string key, Func applySnapshot, Func, Task> applyEvent) + { + Guard.NotNullOrEmpty(key, nameof(key)); + + var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); + return new Persistence(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applySnapshot, applyEvent); } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs index c095e4868..14a93859f 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly IAppPlansProvider appPlansProvider = A.Fake(); private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); private readonly IUserResolver userResolver = A.Fake(); - private readonly AppDomainObject app; + private readonly AppDomainObject app = new AppDomainObject(); private readonly Language language = Language.DE; private readonly string contributorId = Guid.NewGuid().ToString(); private readonly string clientName = "client"; @@ -34,8 +34,6 @@ namespace Squidex.Domain.Apps.Entities.Apps public AppCommandMiddlewareTests() { - app = new AppDomainObject(); - A.CallTo(() => appProvider.GetAppAsync(AppName)) .Returns((IAppEntity)null); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index acac9d548..4dc8b389d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -24,12 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly string clientId = "client"; private readonly string clientNewName = "My Client"; private readonly string planId = "premium"; - private readonly AppDomainObject sut; - - public AppDomainObjectTests() - { - sut = new AppDomainObject(); - } + private readonly AppDomainObject sut = new AppDomainObject(); [Fact] public void Create_should_throw_exception_if_created() diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs new file mode 100644 index 000000000..fbd1bbd7a --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -0,0 +1,137 @@ +// ========================================================================== +// AssetCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetCommandMiddlewareTests : HandlerTestBase + { + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly IAssetStore assetStore = A.Fake(); + private readonly Guid assetId = Guid.NewGuid(); + private readonly Stream stream = new MemoryStream(); + private readonly ImageInfo image = new ImageInfo(2048, 2048); + private readonly AssetDomainObject asset = new AssetDomainObject(); + private readonly AssetFile file; + private readonly AssetCommandMiddleware sut; + + public AssetCommandMiddlewareTests() + { + file = new AssetFile("my-image.png", "image/png", 1024, () => stream); + + sut = new AssetCommandMiddleware(Handler, assetStore, assetThumbnailGenerator); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file }); + + SetupStore(0, context.ContextId); + SetupImageInfo(); + + await TestCreate(asset, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(assetId, context.Result>().IdOrValue); + + AssertAssetHasBeenUploaded(0, context.ContextId); + AssertAssetImageChecked(); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file }); + + SetupStore(1, context.ContextId); + SetupImageInfo(); + + CreateAsset(); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(context); + }); + + AssertAssetHasBeenUploaded(1, context.ContextId); + AssertAssetImageChecked(); + } + + [Fact] + public async Task Rename_should_update_domain_object() + { + CreateAsset(); + + var context = CreateContextForCommand(new RenameAsset { AssetId = assetId, FileName = "my-new-image.png" }); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateAsset(); + + var command = CreateContextForCommand(new DeleteAsset { AssetId = assetId }); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(command); + }); + } + + private void CreateAsset() + { + asset.Create(CreateCommand(new CreateAsset { File = file })); + } + + private void SetupImageInfo() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(image); + } + + private void SetupStore(long version, Guid commitId) + { + A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)) + .Returns(TaskHelper.Done); + A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)) + .Returns(TaskHelper.Done); + A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())) + .Returns(TaskHelper.Done); + } + + private void AssertAssetImageChecked() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)).MustHaveHappened(); + } + + private void AssertAssetHasBeenUploaded(long version, Guid commitId) + { + A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)).MustHaveHappened(); + A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)).MustHaveHappened(); + A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())).MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs new file mode 100644 index 000000000..081ffecd4 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs @@ -0,0 +1,209 @@ +// ========================================================================== +// AssetDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetDomainObjectTests : HandlerTestBase + { + private readonly ImageInfo image = new ImageInfo(2048, 2048); + private readonly Guid assetId = Guid.NewGuid(); + private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); + private readonly AssetDomainObject sut = new AssetDomainObject(); + + [Fact] + public void Create_should_throw_exception_if_created() + { + CreateAsset(); + + Assert.Throws(() => + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file })); + }); + } + + [Fact] + public void Create_should_create_events() + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file, ImageInfo = image })); + + Assert.Equal(0, sut.State.FileVersion); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetCreated + { + IsImage = true, + FileName = file.FileName, + FileSize = file.FileSize, + FileVersion = 0, + MimeType = file.MimeType, + PixelWidth = image.PixelWidth, + PixelHeight = image.PixelHeight + }) + ); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_asset_is_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset())); + }); + } + + [Fact] + public void Update_should_create_events() + { + CreateAsset(); + + sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image })); + + Assert.Equal(1, sut.State.FileVersion); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetUpdated + { + IsImage = true, + FileSize = file.FileSize, + FileVersion = 1, + MimeType = file.MimeType, + PixelWidth = image.PixelWidth, + PixelHeight = image.PixelHeight + }) + ); + } + + [Fact] + public void Rename_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "new-file.png" })); + }); + } + + [Fact] + public void Rename_should_throw_exception_if_asset_is_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset())); + }); + } + + [Fact] + public void Rename_should_create_events() + { + CreateAsset(); + + sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "my-new-image.png" })); + + Assert.Equal("my-new-image.png", sut.State.FileName); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" }) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + }); + } + + [Fact] + public void Delete_should_create_events_with_total_file_size() + { + CreateAsset(); + UpdateAsset(); + + sut.Delete(CreateAssetCommand(new DeleteAsset())); + + Assert.True(sut.State.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 }) + ); + } + + private void CreateAsset() + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file })); + sut.ClearUncommittedEvents(); + } + + private void UpdateAsset() + { + sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); + sut.ClearUncommittedEvents(); + } + + private void DeleteAsset() + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + sut.ClearUncommittedEvents(); + } + + protected T CreateAssetEvent(T @event) where T : AssetEvent + { + @event.AssetId = assetId; + + return CreateEvent(@event); + } + + protected T CreateAssetCommand(T command) where T : AssetAggregateCommand + { + command.AssetId = assetId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs new file mode 100644 index 000000000..92d676354 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// GuardAssetTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets.Guards +{ + public class GuardAssetTests + { + [Fact] + public void CanRename_should_throw_exception_if_name_not_defined() + { + var command = new RenameAsset(); + + Assert.Throws(() => GuardAsset.CanRename(command, "asset-name")); + } + + [Fact] + public void CanRename_should_throw_exception_if_name_are_the_same() + { + var command = new RenameAsset { FileName = "asset-name" }; + + Assert.Throws(() => GuardAsset.CanRename(command, "asset-name")); + } + + [Fact] + public void CanRename_not_should_throw_exception_if_name_are_different() + { + var command = new RenameAsset { FileName = "new-name" }; + + GuardAsset.CanRename(command, "asset-name"); + } + + [Fact] + public void CanCreate_should_not_throw_exception() + { + var command = new CreateAsset(); + + GuardAsset.CanCreate(command); + } + + [Fact] + public void CanUpdate_should_not_throw_exception() + { + var command = new UpdateAsset(); + + GuardAsset.CanUpdate(command); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteAsset(); + + GuardAsset.CanDelete(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs index aa9b3abe9..3b04844ff 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs @@ -23,19 +23,17 @@ namespace Squidex.Domain.Apps.Entities.Rules public class RuleCommandMiddlewareTests : HandlerTestBase { private readonly IAppProvider appProvider = A.Fake(); - private readonly RuleCommandMiddleware sut; - private readonly RuleDomainObject rule; + private readonly RuleDomainObject rule = new RuleDomainObject(); private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; private readonly Guid ruleId = Guid.NewGuid(); + private readonly RuleCommandMiddleware sut; public RuleCommandMiddlewareTests() { A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) .Returns(A.Fake()); - rule = new RuleDomainObject(); - sut = new RuleCommandMiddleware(Handler, appProvider); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs index 10dd3948b..4f4376e56 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs @@ -21,16 +21,10 @@ namespace Squidex.Domain.Apps.Entities.Rules { public class RuleDomainObjectTests : HandlerTestBase { + private readonly Guid ruleId = Guid.NewGuid(); private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger(); private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") }; - private readonly RuleDomainObject sut; - - public Guid RuleId { get; } = Guid.NewGuid(); - - public RuleDomainObjectTests() - { - sut = new RuleDomainObject(); - } + private readonly RuleDomainObject sut = new RuleDomainObject(); [Fact] public void Create_should_throw_exception_if_created() @@ -234,14 +228,14 @@ namespace Squidex.Domain.Apps.Entities.Rules protected T CreateRuleEvent(T @event) where T : RuleEvent { - @event.RuleId = RuleId; + @event.RuleId = ruleId; return CreateEvent(@event); } protected T CreateRuleCommand(T command) where T : RuleAggregateCommand { - command.RuleId = RuleId; + command.RuleId = ruleId; return CreateCommand(command); }