diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs index 244aac11b..37f075a65 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs @@ -5,7 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; @@ -27,6 +29,18 @@ namespace Squidex.Domain.Apps.Core.ValidateContent } } + public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, Func message) + { + var validator = new ContentValidator(schema, partitionResolver, context); + + await validator.ValidateAsync(data); + + if (validator.Errors.Count > 0) + { + throw new ValidationException(message(), validator.Errors.ToList()); + } + } + public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors) { var validator = new ContentValidator(schema, partitionResolver, context); @@ -38,5 +52,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent errors.Add(error); } } + + public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, Func message) + { + var validator = new ContentValidator(schema, partitionResolver, context); + + await validator.ValidatePartialAsync(data); + + if (validator.Errors.Count > 0) + { + throw new ValidationException(message(), validator.Errors.ToList()); + } + } } } diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index b2e028a12..7439e4a1e 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -47,14 +47,14 @@ namespace Squidex.Domain.Apps.Entities public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) { - var app = await stateFactory.GetSingleAsync(appId); + var app = await stateFactory.GetSingleAsync(appId); if (!IsFound(app)) { return (null, null); } - var schema = await stateFactory.GetSingleAsync(id); + var schema = await stateFactory.GetSingleAsync(id); if (!IsFound(schema) || schema.Snapshot.IsDeleted) { @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities return null; } - return (await stateFactory.GetSingleAsync(appId)).Snapshot; + return (await stateFactory.GetSingleAsync(appId)).Snapshot; } public async Task GetSchemaAsync(Guid appId, string name) @@ -85,12 +85,12 @@ namespace Squidex.Domain.Apps.Entities return null; } - return (await stateFactory.GetSingleAsync(schemaId)).Snapshot; + return (await stateFactory.GetSingleAsync(schemaId)).Snapshot; } public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) { - var schema = await stateFactory.GetSingleAsync(id); + var schema = await stateFactory.GetSingleAsync(id); if (!IsFound(schema) || (schema.Snapshot.IsDeleted && !allowDeleted) || schema.Snapshot.AppId.Id != appId) { @@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities var schemas = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return schemas.Where(IsFound).Select(s => (ISchemaEntity)s.Snapshot).ToList(); } @@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities var rules = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return rules.Where(IsFound).Select(r => (IRuleEntity)r.Snapshot).ToList(); } @@ -128,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities var apps = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id))); + ids.Select(id => stateFactory.GetSingleAsync(id))); return apps.Where(IsFound).Select(a => (IAppEntity)a.Snapshot).ToList(); } @@ -143,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities return await schemaRepository.FindSchemaIdAsync(appId, name); } - private static bool IsFound(IDomainObject app) + private static bool IsFound(IDomainObjectGrain app) { return app.Version > EtagVersion.Empty; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs deleted file mode 100644 index 829c1388c..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ /dev/null @@ -1,200 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Guards; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly IAppProvider appProvider; - private readonly IAppPlansProvider appPlansProvider; - private readonly IAppPlanBillingManager appPlansBillingManager; - private readonly IUserResolver userResolver; - - public AppCommandMiddleware( - IAggregateHandler handler, - IAppProvider appProvider, - IAppPlansProvider appPlansProvider, - IAppPlanBillingManager appPlansBillingManager, - IUserResolver userResolver) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(userResolver, nameof(userResolver)); - Guard.NotNull(appPlansProvider, nameof(appPlansProvider)); - Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager)); - - this.handler = handler; - this.userResolver = userResolver; - this.appProvider = appProvider; - this.appPlansProvider = appPlansProvider; - this.appPlansBillingManager = appPlansBillingManager; - } - - protected async Task On(CreateApp command, CommandContext context) - { - var app = await handler.CreateSyncedAsync(context, async a => - { - await GuardApp.CanCreate(command, appProvider); - - a.Create(command); - - context.Complete(EntityCreatedResult.Create(command.AppId, a.Version)); - }); - } - - protected Task On(AssignContributor command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, async a => - { - await GuardAppContributors.CanAssign(a.Snapshot.Contributors, command, userResolver, appPlansProvider.GetPlan(a.Snapshot.Plan?.PlanId)); - - a.AssignContributor(command); - }); - } - - protected Task On(RemoveContributor command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppContributors.CanRemove(a.Snapshot.Contributors, command); - - a.RemoveContributor(command); - }); - } - - protected Task On(AttachClient command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppClients.CanAttach(a.Snapshot.Clients, command); - - a.AttachClient(command); - }); - } - - protected Task On(UpdateClient command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppClients.CanUpdate(a.Snapshot.Clients, command); - - a.UpdateClient(command); - }); - } - - protected Task On(RevokeClient command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppClients.CanRevoke(a.Snapshot.Clients, command); - - a.RevokeClient(command); - }); - } - - protected Task On(AddLanguage command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppLanguages.CanAdd(a.Snapshot.LanguagesConfig, command); - - a.AddLanguage(command); - }); - } - - protected Task On(RemoveLanguage command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppLanguages.CanRemove(a.Snapshot.LanguagesConfig, command); - - a.RemoveLanguage(command); - }); - } - - protected Task On(UpdateLanguage command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppLanguages.CanUpdate(a.Snapshot.LanguagesConfig, command); - - a.UpdateLanguage(command); - }); - } - - protected Task On(AddPattern command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppPattern.CanAdd(a.Snapshot.Patterns, command); - - a.AddPattern(command); - }); - } - - protected Task On(DeletePattern command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAppPattern.CanDelete(a.Snapshot.Patterns, command); - - a.DeletePattern(command); - }); - } - - protected async Task On(UpdatePattern command, CommandContext context) - { - await handler.UpdateSyncedAsync(context, a => - { - GuardAppPattern.CanUpdate(a.Snapshot.Patterns, command); - - a.UpdatePattern(command); - }); - } - - protected Task On(ChangePlan command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, async a => - { - GuardApp.CanChangePlan(command, a.Snapshot.Plan, appPlansProvider); - - if (command.FromCallback) - { - a.ChangePlan(command); - } - else - { - var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Snapshot.Id, a.Snapshot.Name, command.PlanId); - - if (result is PlanChangedResult) - { - a.ChangePlan(command); - } - - context.Complete(result); - } - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - await this.DispatchActionAsync(context.Command, context); - await next(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs deleted file mode 100644 index 2dedb3a7a..000000000 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ /dev/null @@ -1,228 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppDomainObject : SquidexDomainObjectBase - { - private readonly InitialPatterns initialPatterns; - - public AppDomainObject(InitialPatterns initialPatterns) - { - Guard.NotNull(initialPatterns, nameof(initialPatterns)); - - this.initialPatterns = initialPatterns; - } - - public AppDomainObject Create(CreateApp command) - { - ThrowIfCreated(); - - var appId = new NamedId(command.AppId, command.Name); - - var events = new List - { - CreateInitalEvent(command.Name), - CreateInitialOwner(command.Actor), - CreateInitialLanguage() - }; - - foreach (var pattern in initialPatterns) - { - events.Add(CreateInitialPattern(pattern.Key, pattern.Value)); - } - - foreach (var @event in events) - { - @event.Actor = command.Actor; - @event.AppId = appId; - - RaiseEvent(@event); - } - - return this; - } - - public AppDomainObject UpdateLanguage(UpdateLanguage command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); - - return this; - } - - public AppDomainObject UpdateClient(UpdateClient command) - { - ThrowIfNotCreated(); - - if (!string.IsNullOrWhiteSpace(command.Name)) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); - } - - if (command.Permission.HasValue) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value })); - } - - return this; - } - - public AppDomainObject AssignContributor(AssignContributor command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned())); - - return this; - } - - public AppDomainObject RemoveContributor(RemoveContributor command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); - - return this; - } - - public AppDomainObject AttachClient(AttachClient command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); - - return this; - } - - public AppDomainObject RevokeClient(RevokeClient command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); - - return this; - } - - public AppDomainObject AddLanguage(AddLanguage command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); - - return this; - } - - public AppDomainObject RemoveLanguage(RemoveLanguage command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); - - return this; - } - - public AppDomainObject ChangePlan(ChangePlan command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); - - return this; - } - - public AppDomainObject AddPattern(AddPattern command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); - - return this; - } - - public AppDomainObject DeletePattern(DeletePattern command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); - - return this; - } - - public AppDomainObject UpdatePattern(UpdatePattern command) - { - ThrowIfNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); - - return this; - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = new NamedId(Snapshot.Id, Snapshot.Name); - } - - RaiseEvent(Envelope.Create(@event)); - } - - private static AppCreated CreateInitalEvent(string name) - { - return new AppCreated { Name = name }; - } - - private static AppPatternAdded CreateInitialPattern(Guid id, AppPattern pattern) - { - return new AppPatternAdded { PatternId = id, Name = pattern.Name, Pattern = pattern.Pattern, Message = pattern.Message }; - } - - private static AppLanguageAdded CreateInitialLanguage() - { - return new AppLanguageAdded { Language = Language.EN }; - } - - private static AppContributorAssigned CreateInitialOwner(RefToken actor) - { - return new AppContributorAssigned { ContributorId = actor.Identifier, Permission = AppContributorPermission.Owner }; - } - - private void ThrowIfNotCreated() - { - if (string.IsNullOrWhiteSpace(Snapshot.Name)) - { - throw new DomainException("App has not been created."); - } - } - - private void ThrowIfCreated() - { - if (!string.IsNullOrWhiteSpace(Snapshot.Name)) - { - throw new DomainException("App has already been created."); - } - } - - public override void ApplyEvent(Envelope @event) - { - ApplySnapshot(Snapshot.Apply(@event)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs new file mode 100644 index 000000000..50a342d7e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -0,0 +1,314 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Guards; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppGrain : DomainObjectGrain + { + private readonly InitialPatterns initialPatterns; + private readonly IAppProvider appProvider; + private readonly IAppPlansProvider appPlansProvider; + private readonly IAppPlanBillingManager appPlansBillingManager; + private readonly IUserResolver userResolver; + + public AppGrain( + InitialPatterns initialPatterns, + IStore store, + IAppProvider appProvider, + IAppPlansProvider appPlansProvider, + IAppPlanBillingManager appPlansBillingManager, + IUserResolver userResolver) + : base(store) + { + Guard.NotNull(initialPatterns, nameof(initialPatterns)); + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(userResolver, nameof(userResolver)); + Guard.NotNull(appPlansProvider, nameof(appPlansProvider)); + Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager)); + + this.userResolver = userResolver; + this.appProvider = appProvider; + this.appPlansProvider = appPlansProvider; + this.appPlansBillingManager = appPlansBillingManager; + this.initialPatterns = initialPatterns; + } + + public override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateApp createApp: + return CreateAsync(createApp, async c => + { + await GuardApp.CanCreate(c, appProvider); + + Create(c); + }); + + case AssignContributor assigneContributor: + return UpdateAsync(assigneContributor, async c => + { + await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId)); + + AssignContributor(c); + }); + + case RemoveContributor removeContributor: + return UpdateAsync(removeContributor, c => + { + GuardAppContributors.CanRemove(Snapshot.Contributors, c); + + RemoveContributor(c); + }); + + case AttachClient attachClient: + return UpdateAsync(attachClient, c => + { + GuardAppClients.CanAttach(Snapshot.Clients, c); + + AttachClient(c); + }); + + case UpdateClient updateClient: + return UpdateAsync(updateClient, c => + { + GuardAppClients.CanUpdate(Snapshot.Clients, c); + + UpdateClient(c); + }); + + case RevokeClient revokeClient: + return UpdateAsync(revokeClient, c => + { + GuardAppClients.CanRevoke(Snapshot.Clients, c); + + RevokeClient(c); + }); + + case AddLanguage addLanguage: + return UpdateAsync(addLanguage, c => + { + GuardAppLanguages.CanAdd(Snapshot.LanguagesConfig, c); + + AddLanguage(c); + }); + + case RemoveLanguage removeLanguage: + return UpdateAsync(removeLanguage, c => + { + GuardAppLanguages.CanRemove(Snapshot.LanguagesConfig, c); + + RemoveLanguage(c); + }); + + case UpdateLanguage updateLanguage: + return UpdateAsync(updateLanguage, c => + { + GuardAppLanguages.CanUpdate(Snapshot.LanguagesConfig, c); + + UpdateLanguage(c); + }); + + case AddPattern addPattern: + return UpdateAsync(addPattern, c => + { + GuardAppPattern.CanAdd(Snapshot.Patterns, c); + + AddPattern(c); + }); + + case DeletePattern deletePattern: + return UpdateAsync(deletePattern, c => + { + GuardAppPattern.CanDelete(Snapshot.Patterns, c); + + DeletePattern(c); + }); + + case UpdatePattern updatePattern: + return UpdateAsync(updatePattern, c => + { + GuardAppPattern.CanUpdate(Snapshot.Patterns, c); + + UpdatePattern(c); + }); + + case ChangePlan changePlan: + return UpdateReturnAsync(changePlan, async c => + { + GuardApp.CanChangePlan(c, Snapshot.Plan, appPlansProvider); + + if (c.FromCallback) + { + ChangePlan(c); + + return null; + } + else + { + var result = await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.Id, Snapshot.Name, c.PlanId); + + if (result is PlanChangedResult) + { + ChangePlan(c); + } + + return result; + } + }); + + default: + throw new NotSupportedException(); + } + } + + public void Create(CreateApp command) + { + var appId = new NamedId(command.AppId, command.Name); + + var events = new List + { + CreateInitalEvent(command.Name), + CreateInitialOwner(command.Actor), + CreateInitialLanguage() + }; + + foreach (var pattern in initialPatterns) + { + events.Add(CreateInitialPattern(pattern.Key, pattern.Value)); + } + + foreach (var @event in events) + { + @event.Actor = command.Actor; + @event.AppId = appId; + + RaiseEvent(@event); + } + } + + public void UpdateClient(UpdateClient command) + { + if (!string.IsNullOrWhiteSpace(command.Name)) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); + } + + if (command.Permission.HasValue) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value })); + } + } + + public void UpdateLanguage(UpdateLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); + } + + public void AssignContributor(AssignContributor command) + { + RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned())); + } + + public void RemoveContributor(RemoveContributor command) + { + RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); + } + + public void AttachClient(AttachClient command) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); + } + + public void RevokeClient(RevokeClient command) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); + } + + public void AddLanguage(AddLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); + } + + public void RemoveLanguage(RemoveLanguage command) + { + RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); + } + + public void ChangePlan(ChangePlan command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); + } + + public void AddPattern(AddPattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); + } + + public void DeletePattern(DeletePattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); + } + + public void UpdatePattern(UpdatePattern command) + { + RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = new NamedId(Snapshot.Id, Snapshot.Name); + } + + RaiseEvent(Envelope.Create(@event)); + } + + private static AppCreated CreateInitalEvent(string name) + { + return new AppCreated { Name = name }; + } + + private static AppPatternAdded CreateInitialPattern(Guid id, AppPattern pattern) + { + return new AppPatternAdded { PatternId = id, Name = pattern.Name, Pattern = pattern.Pattern, Message = pattern.Message }; + } + + private static AppLanguageAdded CreateInitialLanguage() + { + return new AppLanguageAdded { Language = Language.EN }; + } + + private static AppContributorAssigned CreateInitialOwner(RefToken actor) + { + return new AppContributorAssigned { ContributorId = actor.Identifier, Permission = AppContributorPermission.Owner }; + } + + public override void ApplyEvent(Envelope @event) + { + ApplySnapshot(Snapshot.Apply(@event)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs index b46cf9240..1069ebee2 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards if (!clients.TryGetValue(id, out var client)) { - throw new DomainObjectNotFoundException(id, "Clients", typeof(AppDomainObject)); + throw new DomainObjectNotFoundException(id, "Clients", typeof(IAppEntity)); } return client; diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs index 08860f0e9..dfaedfc24 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards if (!contributors.ContainsKey(command.ContributorId)) { - throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(AppDomainObject)); + throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity)); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs index a860def06..bacb27b5b 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards if (!languages.TryGetConfig(language, out var languageConfig)) { - throw new DomainObjectNotFoundException(language, "Languages", typeof(AppDomainObject)); + throw new DomainObjectNotFoundException(language, "Languages", typeof(IAppEntity)); } return languageConfig; diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs index 3d020f3f9..2d82d145b 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations public Task ChangePlanAsync(string userId, Guid appId, string appName, string planId) { - return Task.FromResult(PlanChangedResult.Instance); + return Task.FromResult(new PlanChangedResult()); } public Task GetPortalLinkAsync(string userId) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs index 4b71408cb..12b46f875 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs @@ -9,10 +9,5 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services { public sealed class PlanChangeAsyncResult : IChangePlanResult { - public static readonly PlanChangeAsyncResult Instance = new PlanChangeAsyncResult(); - - private PlanChangeAsyncResult() - { - } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs index dc424c993..5361af735 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs @@ -9,10 +9,5 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services { public sealed class PlanChangedResult : IChangePlanResult { - public static readonly PlanChangedResult Instance = new PlanChangedResult(); - - private PlanChangedResult() - { - } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index d491c5c12..734ddb57c 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -8,108 +8,80 @@ using System; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Domain.Apps.Entities.Assets.Guards; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetCommandMiddleware : ICommandMiddleware + public sealed class AssetCommandMiddleware : GrainCommandMiddleware { - private readonly IAggregateHandler handler; private readonly IAssetStore assetStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; public AssetCommandMiddleware( - IAggregateHandler handler, + IStateFactory stateFactory, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator) + : base(stateFactory) { - Guard.NotNull(handler, nameof(handler)); Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); - this.handler = handler; this.assetStore = assetStore; this.assetThumbnailGenerator = assetThumbnailGenerator; } - protected async Task On(CreateAsset command, CommandContext context) + public async override Task HandleAsync(CommandContext context, Func next) { - command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); - try + switch (context.Command) { - var asset = await handler.CreateSyncedAsync(context, async a => - { - GuardAsset.CanCreate(command); - - a.Create(command); - - await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); - - context.Complete(EntityCreatedResult.Create(command.AssetId, a.Version)); - }); - - await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), command.AssetId.ToString(), asset.Snapshot.FileVersion, null); - } - finally - { - await assetStore.DeleteTemporaryAsync(context.ContextId.ToString()); - } - } - - protected async Task On(UpdateAsset command, CommandContext context) - { - command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); - - try - { - var asset = await handler.UpdateSyncedAsync(context, async a => - { - GuardAsset.CanUpdate(command); - - a.Update(command); - - await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); - - context.Complete(new AssetSavedResult(a.Version, a.Snapshot.FileVersion)); - }); - - await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), command.AssetId.ToString(), asset.Snapshot.FileVersion, null); - } - finally - { - await assetStore.DeleteTemporaryAsync(context.ContextId.ToString()); - } - } - - protected Task On(RenameAsset command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAsset.CanRename(command, a.Snapshot.FileName); - - a.Rename(command); - }); - } - - protected Task On(DeleteAsset command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, a => - { - GuardAsset.CanDelete(command); - - a.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - if (!await this.DispatchActionAsync(context.Command, context)) - { - await next(); + case CreateAsset createAsset: + { + createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead()); + + await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), createAsset.File.OpenRead()); + try + { + var result = await ExecuteCommandAsync(createAsset) as AssetSavedResult; + + context.Complete(EntityCreatedResult.Create(createAsset.AssetId, result.Version)); + + await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null); + } + finally + { + await assetStore.DeleteTemporaryAsync(context.ContextId.ToString()); + } + + break; + } + + case UpdateAsset updateAsset: + { + updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead()); + + await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), updateAsset.File.OpenRead()); + try + { + var result = await ExecuteCommandAsync(updateAsset) as AssetSavedResult; + + context.Complete(result); + + await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null); + } + finally + { + await assetStore.DeleteTemporaryAsync(context.ContextId.ToString()); + } + + break; + } + + default: + await base.HandleAsync(context, next); + break; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs similarity index 55% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs rename to src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index 7cc351c83..19858fbf1 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -5,22 +5,71 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.Guards; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetDomainObject : SquidexDomainObjectBase + public class AssetGrain : DomainObjectGrain { - public AssetDomainObject Create(CreateAsset command) + public AssetGrain(IStore store) + : base(store) { - VerifyNotCreated(); + } + + public override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAsset createRule: + return CreateReturnAsync(createRule, c => + { + GuardAsset.CanCreate(c); + + Create(c); + + return new AssetSavedResult(NewVersion, Snapshot.FileVersion); + }); + case UpdateAsset updateRule: + return UpdateReturnAsync(updateRule, c => + { + GuardAsset.CanUpdate(c); + + Update(c); + + return new AssetSavedResult(NewVersion, Snapshot.FileVersion); + }); + case RenameAsset renameAsset: + return UpdateAsync(renameAsset, c => + { + GuardAsset.CanRename(c, Snapshot.FileName); + + Rename(c); + }); + case DeleteAsset deleteAsset: + return UpdateAsync(deleteAsset, c => + { + GuardAsset.CanDelete(c); + + Delete(c); + }); + default: + throw new NotSupportedException(); + } + } + public void Create(CreateAsset command) + { var @event = SimpleMapper.Map(command, new AssetCreated { FileName = command.File.FileName, @@ -33,13 +82,11 @@ namespace Squidex.Domain.Apps.Entities.Assets }); RaiseEvent(@event); - - return this; } - public AssetDomainObject Update(UpdateAsset command) + public void Update(UpdateAsset command) { - VerifyCreatedAndNotDeleted(); + VerifyNotDeleted(); var @event = SimpleMapper.Map(command, new AssetUpdated { @@ -52,26 +99,20 @@ namespace Squidex.Domain.Apps.Entities.Assets }); RaiseEvent(@event); - - return this; } - public AssetDomainObject Delete(DeleteAsset command) + public void Delete(DeleteAsset command) { - VerifyCreatedAndNotDeleted(); + VerifyNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize })); - - return this; } - public AssetDomainObject Rename(RenameAsset command) + public void Rename(RenameAsset command) { - VerifyCreatedAndNotDeleted(); + VerifyNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new AssetRenamed())); - - return this; } private void RaiseEvent(AppEvent @event) @@ -84,19 +125,11 @@ namespace Squidex.Domain.Apps.Entities.Assets RaiseEvent(Envelope.Create(@event)); } - private void VerifyNotCreated() - { - if (!string.IsNullOrWhiteSpace(Snapshot.FileName)) - { - throw new DomainException("Asset has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() + private void VerifyNotDeleted() { - if (Snapshot.IsDeleted || string.IsNullOrWhiteSpace(Snapshot.FileName)) + if (Snapshot.IsDeleted) { - throw new DomainException("Asset has already been deleted or not created yet."); + throw new DomainException("Asset has already been deleted"); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs deleted file mode 100644 index 7674f1512..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs +++ /dev/null @@ -1,158 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Guards; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly IAppProvider appProvider; - private readonly IAssetRepository assetRepository; - private readonly IContentRepository contentRepository; - private readonly IScriptEngine scriptEngine; - - public ContentCommandMiddleware( - IAggregateHandler handler, - IAppProvider appProvider, - IAssetRepository assetRepository, - IScriptEngine scriptEngine, - IContentRepository contentRepository) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(scriptEngine, nameof(scriptEngine)); - Guard.NotNull(assetRepository, nameof(assetRepository)); - Guard.NotNull(contentRepository, nameof(contentRepository)); - - this.handler = handler; - this.appProvider = appProvider; - this.scriptEngine = scriptEngine; - this.assetRepository = assetRepository; - this.contentRepository = contentRepository; - } - - protected async Task On(CreateContent command, CommandContext context) - { - await handler.CreateAsync(context, async content => - { - GuardContent.CanCreate(command); - - var operationContext = await CreateContext(command, content, () => "Failed to create content."); - - if (command.Publish) - { - await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published"); - } - - await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create"); - await operationContext.EnrichAsync(); - await operationContext.ValidateAsync(false); - - content.Create(command); - - context.Complete(EntityCreatedResult.Create(command.Data, content.Version)); - }); - } - - protected async Task On(UpdateContent command, CommandContext context) - { - await handler.UpdateAsync(context, async content => - { - GuardContent.CanUpdate(command); - - var operationContext = await CreateContext(command, content, () => "Failed to update content."); - - await operationContext.ValidateAsync(true); - await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update"); - - content.Update(command); - - context.Complete(new ContentDataChangedResult(content.Snapshot.Data, content.Version)); - }); - } - - protected async Task On(PatchContent command, CommandContext context) - { - await handler.UpdateAsync(context, async content => - { - GuardContent.CanPatch(command); - - var operationContext = await CreateContext(command, content, () => "Failed to patch content."); - - await operationContext.ValidateAsync(true); - await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch"); - - content.Patch(command); - - context.Complete(new ContentDataChangedResult(content.Snapshot.Data, content.Version)); - }); - } - - protected Task On(ChangeContentStatus command, CommandContext context) - { - return handler.UpdateAsync(context, async content => - { - GuardContent.CanChangeContentStatus(content.Snapshot.Status, command); - - if (!command.DueTime.HasValue) - { - var operationContext = await CreateContext(command, content, () => "Failed to patch content."); - - await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); - } - - content.ChangeStatus(command); - }); - } - - protected Task On(DeleteContent command, CommandContext context) - { - return handler.UpdateAsync(context, async content => - { - GuardContent.CanDelete(command); - - var operationContext = await CreateContext(command, content, () => "Failed to delete content."); - - await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete"); - - content.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - await this.DispatchActionAsync(context.Command, context); - await next(); - } - - private async Task CreateContext(ContentCommand command, ContentDomainObject content, Func message) - { - var operationContext = - await ContentOperationContext.CreateAsync( - contentRepository, - content, - command, - appProvider, - assetRepository, - scriptEngine, - message); - - return operationContext; - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs deleted file mode 100644 index 47db34ab1..000000000 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ /dev/null @@ -1,126 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public sealed class ContentDomainObject : SquidexDomainObjectBase - { - public ContentDomainObject Create(CreateContent command) - { - VerifyNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); - - if (command.Publish) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published })); - } - - return this; - } - - public ContentDomainObject Delete(DeleteContent command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); - - return this; - } - - public ContentDomainObject ChangeStatus(ChangeContentStatus command) - { - VerifyCreatedAndNotDeleted(); - - if (command.DueTime.HasValue) - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); - } - else - { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); - } - - return this; - } - - public ContentDomainObject Update(UpdateContent command) - { - VerifyCreatedAndNotDeleted(); - - if (!command.Data.Equals(Snapshot.Data)) - { - RaiseEvent(SimpleMapper.Map(command, new ContentUpdated())); - } - - return this; - } - - public ContentDomainObject Patch(PatchContent command) - { - VerifyCreatedAndNotDeleted(); - - var newData = command.Data.MergeInto(Snapshot.Data); - - if (!newData.Equals(Snapshot.Data)) - { - var @event = SimpleMapper.Map(command, new ContentUpdated()); - - @event.Data = newData; - - RaiseEvent(@event); - } - - return this; - } - - private void RaiseEvent(SchemaEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - if (@event.SchemaId == null) - { - @event.SchemaId = Snapshot.SchemaId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private void VerifyNotCreated() - { - if (Snapshot.Data != null) - { - throw new DomainException("Content has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (Snapshot.IsDeleted || Snapshot.Data == null) - { - throw new DomainException("Content has already been deleted or not created yet."); - } - } - - public override void ApplyEvent(Envelope @event) - { - ApplySnapshot(Snapshot.Apply(@event)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs new file mode 100644 index 000000000..e1ef9b9ba --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -0,0 +1,232 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Guards; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentGrain : DomainObjectGrain + { + private readonly IAppProvider appProvider; + private readonly IAssetRepository assetRepository; + private readonly IContentRepository contentRepository; + private readonly IScriptEngine scriptEngine; + + public ContentGrain( + IStore store, + IAppProvider appProvider, + IAssetRepository assetRepository, + IScriptEngine scriptEngine, + IContentRepository contentRepository) + : base(store) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(scriptEngine, nameof(scriptEngine)); + Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(contentRepository, nameof(contentRepository)); + + this.appProvider = appProvider; + this.scriptEngine = scriptEngine; + this.assetRepository = assetRepository; + this.contentRepository = contentRepository; + } + + public override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateContent createContent: + return CreateReturnAsync(createContent, async c => + { + GuardContent.CanCreate(c); + + var operationContext = await CreateContext(c, () => "Failed to create content."); + + if (c.Publish) + { + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published"); + } + + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create"); + await operationContext.EnrichAsync(); + await operationContext.ValidateAsync(); + + Create(c); + + return EntityCreatedResult.Create(c.Data, NewVersion); + }); + + case UpdateContent updateContent: + return UpdateReturnAsync(updateContent, async c => + { + GuardContent.CanUpdate(c); + + var operationContext = await CreateContext(c, () => "Failed to update content."); + + await operationContext.ValidateAsync(); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update"); + + Update(c); + + return new ContentDataChangedResult(Snapshot.Data, NewVersion); + }); + + case PatchContent patchContent: + return UpdateReturnAsync(patchContent, async c => + { + GuardContent.CanPatch(c); + + var operationContext = await CreateContext(c, () => "Failed to patch content."); + + await operationContext.ValidatePartialAsync(); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch"); + + Patch(c); + + return new ContentDataChangedResult(Snapshot.Data, NewVersion); + }); + + case ChangeContentStatus patchContent: + return UpdateAsync(patchContent, async c => + { + GuardContent.CanChangeContentStatus(Snapshot.Status, c); + + if (!c.DueTime.HasValue) + { + var operationContext = await CreateContext(c, () => "Failed to patch content."); + + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, c.Status); + } + + ChangeStatus(c); + }); + + case DeleteContent deleteContent: + return UpdateAsync(deleteContent, async c => + { + GuardContent.CanDelete(c); + + var operationContext = await CreateContext(c, () => "Failed to delete content."); + + await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete"); + + Delete(c); + }); + + default: + throw new NotSupportedException(); + } + } + + public void Create(CreateContent command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); + + if (command.Publish) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published })); + } + } + + public void Update(UpdateContent command) + { + if (!command.Data.Equals(Snapshot.Data)) + { + RaiseEvent(SimpleMapper.Map(command, new ContentUpdated())); + } + } + + public void ChangeStatus(ChangeContentStatus command) + { + if (command.DueTime.HasValue) + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); + } + else + { + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); + } + } + + public void Patch(PatchContent command) + { + var newData = command.Data.MergeInto(Snapshot.Data); + + if (!newData.Equals(Snapshot.Data)) + { + var @event = SimpleMapper.Map(command, new ContentUpdated()); + + @event.Data = newData; + + RaiseEvent(@event); + } + } + + public void Delete(DeleteContent command) + { + RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); + } + + private void RaiseEvent(SchemaEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + if (@event.SchemaId == null) + { + @event.SchemaId = Snapshot.SchemaId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Content has already been deleted."); + } + } + + public override void ApplyEvent(Envelope @event) + { + ApplySnapshot(Snapshot.Apply(@event)); + } + + private async Task CreateContext(ContentCommand command, Func message) + { + var operationContext = + await ContentOperationContext.CreateAsync(command, Snapshot, + contentRepository, + appProvider, + assetRepository, + scriptEngine, + message); + + return operationContext; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index fdbcd0386..3cc500552 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.EnrichContent; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.ValidateContent; @@ -17,16 +18,15 @@ using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ContentOperationContext { - private ContentDomainObject content; private ContentCommand command; private IContentRepository contentRepository; + private IContentEntity content; private IAssetRepository assetRepository; private IScriptEngine scriptEngine; private ISchemaEntity schemaEntity; @@ -35,16 +35,16 @@ namespace Squidex.Domain.Apps.Entities.Contents private Func message; public static async Task CreateAsync( - IContentRepository contentRepository, - ContentDomainObject content, ContentCommand command, + IContentEntity content, + IContentRepository contentRepository, IAppProvider appProvider, IAssetRepository assetRepository, IScriptEngine scriptEngine, Func message) { - var a = content.Snapshot.AppId; - var s = content.Snapshot.SchemaId; + var a = content.AppId; + var s = content.SchemaId; if (command is CreateContent createContent) { @@ -80,54 +80,35 @@ namespace Squidex.Domain.Apps.Entities.Contents return TaskHelper.Done; } - public async Task ValidateAsync(bool partial) + public Task ValidateAsync() { if (command is ContentDataCommand dataCommand) { - var errors = new List(); - - var ctx = - new ValidationContext( - (contentIds, schemaId) => - { - return QueryContentsAsync(schemaId, contentIds); - }, - assetIds => - { - return QueryAssetsAsync(assetIds); - }); - - if (partial) - { - await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); - } - else - { - await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); - } + var ctx = CreateValidationContext(); - if (errors.Count > 0) - { - throw new ValidationException(message(), errors.ToArray()); - } + return dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); } - } - private async Task> QueryAssetsAsync(IEnumerable assetIds) - { - return await assetRepository.QueryAsync(appId, new HashSet(assetIds)); + return TaskHelper.Done; } - private async Task> QueryContentsAsync(Guid schemaId, IEnumerable contentIds) + public Task ValidatePartialAsync() { - return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList()); + if (command is ContentDataCommand dataCommand) + { + var ctx = CreateValidationContext(); + + return dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message); + } + + return TaskHelper.Done; } public Task ExecuteScriptAndTransformAsync(Func script, object operation) { if (command is ContentDataCommand dataCommand) { - var ctx = new ScriptContext { ContentId = content.Snapshot.Id, OldData = content.Snapshot.Data, User = command.User, Operation = operation.ToString(), Data = dataCommand.Data }; + var ctx = CreateScriptContext(operation, dataCommand.Data); dataCommand.Data = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity)); } @@ -137,11 +118,39 @@ namespace Squidex.Domain.Apps.Entities.Contents public Task ExecuteScriptAsync(Func script, object operation) { - var ctx = new ScriptContext { ContentId = content.Snapshot.Id, OldData = content.Snapshot.Data, User = command.User, Operation = operation.ToString() }; + var ctx = CreateScriptContext(operation, content.Data); scriptEngine.Execute(ctx, script(schemaEntity)); return TaskHelper.Done; } + + private ScriptContext CreateScriptContext(object operation, NamedContentData data = null) + { + return new ScriptContext { ContentId = command.ContentId, OldData = content.Data, Data = data, User = command.User, Operation = operation.ToString() }; + } + + private ValidationContext CreateValidationContext() + { + return new ValidationContext( + (contentIds, schemaId) => + { + return QueryContentsAsync(schemaId, contentIds); + }, + assetIds => + { + return QueryAssetsAsync(assetIds); + }); + } + + private async Task> QueryAssetsAsync(IEnumerable assetIds) + { + return await assetRepository.QueryAsync(appId, new HashSet(assetIds)); + } + + private async Task> QueryContentsAsync(Guid schemaId, IEnumerable contentIds) + { + return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList()); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs deleted file mode 100644 index 8a6e43441..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ /dev/null @@ -1,89 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.Rules.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class RuleCommandMiddleware : ICommandMiddleware - { - private readonly IAggregateHandler handler; - private readonly IAppProvider appProvider; - - public RuleCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.handler = handler; - - this.appProvider = appProvider; - } - - protected Task On(CreateRule command, CommandContext context) - { - return handler.CreateSyncedAsync(context, async r => - { - await GuardRule.CanCreate(command, appProvider); - - r.Create(command); - }); - } - - protected Task On(UpdateRule command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, async r => - { - await GuardRule.CanUpdate(command, r.Snapshot.AppId.Id, appProvider); - - r.Update(command); - }); - } - - protected Task On(EnableRule command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, r => - { - GuardRule.CanEnable(command, r.Snapshot.RuleDef); - - r.Enable(command); - }); - } - - protected Task On(DisableRule command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, r => - { - GuardRule.CanDisable(command, r.Snapshot.RuleDef); - - r.Disable(command); - }); - } - - protected Task On(DeleteRule command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, r => - { - GuardRule.CanDelete(command); - - r.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - await this.DispatchActionAsync(context.Command, context); - await next(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs deleted file mode 100644 index 54c9afc2d..000000000 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.Rules.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public sealed class RuleDomainObject : SquidexDomainObjectBase - { - public void Create(CreateRule command) - { - VerifyNotCreated(); - - RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); - } - - public void Update(UpdateRule command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); - } - - public void Enable(EnableRule command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); - } - - public void Disable(DisableRule command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); - } - - public void Delete(DeleteRule command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); - } - - private void RaiseEvent(AppEvent @event) - { - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private void VerifyNotCreated() - { - if (Snapshot.RuleDef != null) - { - throw new DomainException("Webhook has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (Snapshot.IsDeleted || Snapshot.RuleDef == null) - { - throw new DomainException("Webhook has already been deleted or not created yet."); - } - } - - public override void ApplyEvent(Envelope @event) - { - ApplySnapshot(Snapshot.Apply(@event)); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs new file mode 100644 index 000000000..ca9b1febb --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.Guards; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleGrain : DomainObjectGrain + { + private readonly IAppProvider appProvider; + + public RuleGrain(IStore store, IAppProvider appProvider) + : base(store) + { + Guard.NotNull(appProvider, nameof(appProvider)); + + this.appProvider = appProvider; + } + + public override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateRule createRule: + return CreateAsync(createRule, c => + { + GuardRule.CanCreate(c, appProvider); + + Create(c); + }); + case UpdateRule updateRule: + return UpdateAsync(updateRule, c => + { + GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider); + + Update(c); + }); + case EnableRule enableRule: + return UpdateAsync(enableRule, c => + { + GuardRule.CanEnable(c, Snapshot.RuleDef); + + Enable(c); + }); + case DisableRule disableRule: + return UpdateAsync(disableRule, c => + { + GuardRule.CanDisable(c, Snapshot.RuleDef); + + Disable(c); + }); + case DeleteRule deleteRule: + return UpdateAsync(deleteRule, c => + { + GuardRule.CanDelete(deleteRule); + + Delete(c); + }); + default: + throw new NotSupportedException(); + } + } + + public void Create(CreateRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleCreated())); + } + + public void Update(UpdateRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleUpdated())); + } + + public void Enable(EnableRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleEnabled())); + } + + public void Disable(DisableRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleDisabled())); + } + + public void Delete(DeleteRule command) + { + RaiseEvent(SimpleMapper.Map(command, new RuleDeleted())); + } + + private void RaiseEvent(AppEvent @event) + { + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Webhook has already been deleted."); + } + } + + public override void ApplyEvent(Envelope @event) + { + ApplySnapshot(Snapshot.Apply(@event)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs deleted file mode 100644 index b58f2c815..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs +++ /dev/null @@ -1,194 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.Guards; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Dispatching; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public class SchemaCommandMiddleware : ICommandMiddleware - { - private readonly IAppProvider appProvider; - private readonly IAggregateHandler handler; - - public SchemaCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider) - { - Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appProvider, nameof(appProvider)); - - this.handler = handler; - - this.appProvider = appProvider; - } - - protected Task On(CreateSchema command, CommandContext context) - { - return handler.CreateSyncedAsync(context, async s => - { - await GuardSchema.CanCreate(command, appProvider); - - s.Create(command); - - context.Complete(EntityCreatedResult.Create(command.SchemaId, s.Version)); - }); - } - - protected Task On(AddField command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchemaField.CanAdd(s.Snapshot.SchemaDef, command); - - s.Add(command); - - context.Complete(EntityCreatedResult.Create(s.Snapshot.SchemaDef.FieldsById.Values.First(x => x.Name == command.Name).Id, s.Version)); - }); - } - - protected Task On(DeleteField command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchemaField.CanDelete(s.Snapshot.SchemaDef, command); - - s.DeleteField(command); - }); - } - - protected Task On(LockField command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchemaField.CanLock(s.Snapshot.SchemaDef, command); - - s.LockField(command); - }); - } - - protected Task On(HideField command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchemaField.CanHide(s.Snapshot.SchemaDef, command); - - s.HideField(command); - }); - } - - protected Task On(ShowField command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchemaField.CanShow(s.Snapshot.SchemaDef, command); - - s.ShowField(command); - }); - } - - protected Task On(DisableField command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchemaField.CanDisable(s.Snapshot.SchemaDef, command); - - s.DisableField(command); - }); - } - - protected Task On(EnableField command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchemaField.CanEnable(s.Snapshot.SchemaDef, command); - - s.EnableField(command); - }); - } - - protected Task On(UpdateField command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchemaField.CanUpdate(s.Snapshot.SchemaDef, command); - - s.UpdateField(command); - }); - } - - protected Task On(ReorderFields command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchema.CanReorder(s.Snapshot.SchemaDef, command); - - s.Reorder(command); - }); - } - - protected Task On(UpdateSchema command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchema.CanUpdate(s.Snapshot.SchemaDef, command); - - s.Update(command); - }); - } - - protected Task On(PublishSchema command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchema.CanPublish(s.Snapshot.SchemaDef, command); - - s.Publish(command); - }); - } - - protected Task On(UnpublishSchema command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchema.CanUnpublish(s.Snapshot.SchemaDef, command); - - s.Unpublish(command); - }); - } - - protected Task On(ConfigureScripts command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchema.CanConfigureScripts(s.Snapshot.SchemaDef, command); - - s.ConfigureScripts(command); - }); - } - - protected Task On(DeleteSchema command, CommandContext context) - { - return handler.UpdateSyncedAsync(context, s => - { - GuardSchema.CanDelete(s.Snapshot.SchemaDef, command); - - s.Delete(command); - }); - } - - public async Task HandleAsync(CommandContext context, Func next) - { - await this.DispatchActionAsync(context.Command, context); - await next(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs deleted file mode 100644 index b57b31333..000000000 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ /dev/null @@ -1,229 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public sealed class SchemaDomainObject : SquidexDomainObjectBase - { - private readonly FieldRegistry registry; - - public SchemaDomainObject(FieldRegistry registry) - { - Guard.NotNull(registry, nameof(registry)); - - this.registry = registry; - } - - public SchemaDomainObject Create(CreateSchema command) - { - VerifyNotCreated(); - - var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId(command.SchemaId, command.Name) }); - - if (command.Fields != null) - { - @event.Fields = new List(); - - foreach (var commandField in command.Fields) - { - var eventField = SimpleMapper.Map(commandField, new SchemaCreatedField()); - - @event.Fields.Add(eventField); - } - } - - RaiseEvent(@event); - - return this; - } - - public SchemaDomainObject Add(AddField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(Snapshot.TotalFields + 1, command.Name) })); - - return this; - } - - public SchemaDomainObject UpdateField(UpdateField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated())); - - return this; - } - - public SchemaDomainObject LockField(LockField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldLocked()); - - return this; - } - - public SchemaDomainObject HideField(HideField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldHidden()); - - return this; - } - - public SchemaDomainObject ShowField(ShowField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldShown()); - - return this; - } - - public SchemaDomainObject DisableField(DisableField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldDisabled()); - - return this; - } - - public SchemaDomainObject EnableField(EnableField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldEnabled()); - - return this; - } - - public SchemaDomainObject DeleteField(DeleteField command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(command, new FieldDeleted()); - - return this; - } - - public SchemaDomainObject Reorder(ReorderFields command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered())); - - return this; - } - - public SchemaDomainObject Publish(PublishSchema command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaPublished())); - - return this; - } - - public SchemaDomainObject Unpublish(UnpublishSchema command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished())); - - return this; - } - - public SchemaDomainObject ConfigureScripts(ConfigureScripts command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured())); - - return this; - } - - public SchemaDomainObject Delete(DeleteSchema command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaDeleted())); - - return this; - } - - public SchemaDomainObject Update(UpdateSchema command) - { - VerifyCreatedAndNotDeleted(); - - RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated())); - - return this; - } - - private void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event) - { - SimpleMapper.Map(fieldCommand, @event); - - if (Snapshot.SchemaDef.FieldsById.TryGetValue(fieldCommand.FieldId, out var field)) - { - @event.FieldId = new NamedId(field.Id, field.Name); - } - - RaiseEvent(@event); - } - - private void RaiseEvent(SchemaEvent @event) - { - if (@event.SchemaId == null) - { - @event.SchemaId = new NamedId(Snapshot.Id, Snapshot.Name); - } - - if (@event.AppId == null) - { - @event.AppId = Snapshot.AppId; - } - - RaiseEvent(Envelope.Create(@event)); - } - - private void VerifyNotCreated() - { - if (Snapshot.SchemaDef != null) - { - throw new DomainException("Schema has already been created."); - } - } - - private void VerifyCreatedAndNotDeleted() - { - if (Snapshot.IsDeleted || Snapshot.SchemaDef == null) - { - throw new DomainException("Schema has already been deleted or not created yet."); - } - } - - public override void ApplyEvent(Envelope @event) - { - ApplySnapshot(Snapshot.Apply(@event, registry)); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs new file mode 100644 index 000000000..2f12f1336 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -0,0 +1,304 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.Guards; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public class SchemaGrain : DomainObjectGrain + { + private readonly IAppProvider appProvider; + private readonly FieldRegistry registry; + + public SchemaGrain(IStore store, IAppProvider appProvider, FieldRegistry registry) + : base(store) + { + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(registry, nameof(registry)); + + this.appProvider = appProvider; + + this.registry = registry; + } + + public override Task ExecuteAsync(IAggregateCommand command) + { + VerifyNotDeleted(); + + switch (command) + { + case CreateSchema createSchema: + return CreateAsync(createSchema, async c => + { + await GuardSchema.CanCreate(c, appProvider); + + Create(c); + }); + + case AddField addField: + return UpdateReturnAsync(addField, c => + { + GuardSchemaField.CanAdd(Snapshot.SchemaDef, c); + + Add(c); + + return EntityCreatedResult.Create(Snapshot.SchemaDef.FieldsById.Values.First(x => x.Name == addField.Name).Id, NewVersion); + }); + + case DeleteField deleteField: + return UpdateAsync(deleteField, c => + { + GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField); + + DeleteField(c); + }); + + case LockField lockField: + return UpdateAsync(lockField, c => + { + GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField); + + LockField(c); + }); + + case HideField hideField: + return UpdateAsync(hideField, c => + { + GuardSchemaField.CanHide(Snapshot.SchemaDef, c); + + HideField(c); + }); + + case ShowField showField: + return UpdateAsync(showField, c => + { + GuardSchemaField.CanShow(Snapshot.SchemaDef, c); + + ShowField(c); + }); + + case DisableField disableField: + return UpdateAsync(disableField, c => + { + GuardSchemaField.CanDisable(Snapshot.SchemaDef, c); + + DisableField(c); + }); + + case EnableField enableField: + return UpdateAsync(enableField, c => + { + GuardSchemaField.CanEnable(Snapshot.SchemaDef, c); + + EnableField(c); + }); + + case UpdateField updateField: + return UpdateAsync(updateField, c => + { + GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c); + + UpdateField(c); + }); + + case ReorderFields reorderFields: + return UpdateAsync(reorderFields, c => + { + GuardSchema.CanReorder(Snapshot.SchemaDef, c); + + Reorder(c); + }); + + case UpdateSchema updateSchema: + return UpdateAsync(updateSchema, c => + { + GuardSchema.CanUpdate(Snapshot.SchemaDef, c); + + Update(c); + }); + + case PublishSchema publishSchema: + return UpdateAsync(publishSchema, c => + { + GuardSchema.CanPublish(Snapshot.SchemaDef, c); + + Publish(c); + }); + + case UnpublishSchema unpublishSchema: + return UpdateAsync(unpublishSchema, c => + { + GuardSchema.CanUnpublish(Snapshot.SchemaDef, c); + + Unpublish(c); + }); + + case ConfigureScripts configureScripts: + return UpdateAsync(configureScripts, c => + { + GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c); + + ConfigureScripts(c); + }); + + case DeleteSchema deleteSchema: + return UpdateAsync(deleteSchema, c => + { + GuardSchema.CanDelete(Snapshot.SchemaDef, c); + + Delete(c); + }); + + default: + throw new NotSupportedException(); + } + } + + public void Create(CreateSchema command) + { + var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId(command.SchemaId, command.Name) }); + + if (command.Fields != null) + { + @event.Fields = new List(); + + foreach (var commandField in command.Fields) + { + var eventField = SimpleMapper.Map(commandField, new SchemaCreatedField()); + + @event.Fields.Add(eventField); + } + } + + RaiseEvent(@event); + } + + public void Add(AddField command) + { + RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(Snapshot.TotalFields + 1, command.Name) })); + } + + public void UpdateField(UpdateField command) + { + RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated())); + } + + public void LockField(LockField command) + { + RaiseEvent(command, new FieldLocked()); + } + + public void HideField(HideField command) + { + RaiseEvent(command, new FieldHidden()); + } + + public void ShowField(ShowField command) + { + RaiseEvent(command, new FieldShown()); + } + + public void DisableField(DisableField command) + { + RaiseEvent(command, new FieldDisabled()); + } + + public void EnableField(EnableField command) + { + RaiseEvent(command, new FieldEnabled()); + } + + public void DeleteField(DeleteField command) + { + RaiseEvent(command, new FieldDeleted()); + } + + public void Reorder(ReorderFields command) + { + RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered())); + } + + public void Publish(PublishSchema command) + { + RaiseEvent(SimpleMapper.Map(command, new SchemaPublished())); + } + + public void Unpublish(UnpublishSchema command) + { + RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished())); + } + + public void ConfigureScripts(ConfigureScripts command) + { + RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured())); + } + + public void Delete(DeleteSchema command) + { + RaiseEvent(SimpleMapper.Map(command, new SchemaDeleted())); + } + + public void Update(UpdateSchema command) + { + RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated())); + } + + private void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event) + { + SimpleMapper.Map(fieldCommand, @event); + + if (Snapshot.SchemaDef.FieldsById.TryGetValue(fieldCommand.FieldId, out var field)) + { + @event.FieldId = new NamedId(field.Id, field.Name); + } + + RaiseEvent(@event); + } + + private void RaiseEvent(SchemaEvent @event) + { + if (@event.SchemaId == null) + { + @event.SchemaId = new NamedId(Snapshot.Id, Snapshot.Name); + } + + if (@event.AppId == null) + { + @event.AppId = Snapshot.AppId; + } + + RaiseEvent(Envelope.Create(@event)); + } + + private void VerifyNotDeleted() + { + if (Snapshot.IsDeleted) + { + throw new DomainException("Schema has already been deleted."); + } + } + + public override void ApplyEvent(Envelope @event) + { + ApplySnapshot(Snapshot.Apply(@event, registry)); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs b/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs deleted file mode 100644 index 63f0f94ed..000000000 --- a/src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Domain.Apps.Entities -{ - public abstract class SquidexDomainObjectBase : DomainObjectBase where T : IDomainState, new() - { - public override void RaiseEvent(Envelope @event) - { - if (@event.Payload is AppEvent appEvent) - { - @event.SetAppId(appEvent.AppId.Id); - } - - base.RaiseEvent(@event); - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs index d2ccafcfd..e1e5b84e4 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs @@ -81,8 +81,6 @@ namespace Squidex.Infrastructure.MongoDb return BsonNull.Value; case JTokenType.Undefined: return BsonUndefined.Value; - case JTokenType.Date: - return BsonValue.Create(((JValue)source).ToString("yyyy-MM-ddTHH:mm:ssK")); case JTokenType.Bytes: return BsonValue.Create(((JValue)source).Value); case JTokenType.Guid: @@ -91,6 +89,23 @@ namespace Squidex.Infrastructure.MongoDb return BsonValue.Create(((JValue)source).ToString()); case JTokenType.TimeSpan: return BsonValue.Create(((JValue)source).ToString()); + case JTokenType.Date: + { + var value = ((JValue)source).Value; + + if (value is DateTime dateTime) + { + return dateTime.ToString("yyyy-MM-ddTHH:mm:ssK"); + } + else if (value is DateTimeOffset dateTimeOffset) + { + return dateTimeOffset.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK"); + } + else + { + return value.ToString(); + } + } } throw new NotSupportedException($"Cannot convert {source.GetType()} to Bson."); diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 27cfdca13..d8c35ef2d 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -68,13 +68,16 @@ namespace Squidex.Infrastructure.MongoDb { return new Lazy>(() => { - var databaseCollection = mongoDatabase.GetCollection( - CollectionName(), - CollectionSettings() ?? new MongoCollectionSettings()); + return Task.Run(async () => + { + var databaseCollection = mongoDatabase.GetCollection( + CollectionName(), + CollectionSettings() ?? new MongoCollectionSettings()); - SetupCollectionAsync(databaseCollection).Wait(); + await SetupCollectionAsync(databaseCollection).ConfigureAwait(false); - return databaseCollection; + return databaseCollection; + }).Result; }); } diff --git a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/Commands/AggregateHandler.cs deleted file mode 100644 index a05e4a5b7..000000000 --- a/src/Squidex.Infrastructure/Commands/AggregateHandler.cs +++ /dev/null @@ -1,149 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public sealed class AggregateHandler : IAggregateHandler - { - private readonly AsyncLockPool lockPool = new AsyncLockPool(10000); - private readonly IStateFactory stateFactory; - private readonly IServiceProvider serviceProvider; - - public AggregateHandler(IStateFactory stateFactory, IServiceProvider serviceProvider) - { - Guard.NotNull(stateFactory, nameof(stateFactory)); - Guard.NotNull(serviceProvider, nameof(serviceProvider)); - - this.stateFactory = stateFactory; - this.serviceProvider = serviceProvider; - } - - public Task CreateAsync(CommandContext context, Func creator) where T : class, IDomainObject - { - Guard.NotNull(creator, nameof(creator)); - - return InvokeAsync(context, creator, false); - } - - public Task UpdateAsync(CommandContext context, Func updater) where T : class, IDomainObject - { - Guard.NotNull(updater, nameof(updater)); - - return InvokeAsync(context, updater, true); - } - - public Task CreateSyncedAsync(CommandContext context, Func creator) where T : class, IDomainObject - { - Guard.NotNull(creator, nameof(creator)); - - return InvokeSyncedAsync(context, creator, false); - } - - public Task UpdateSyncedAsync(CommandContext context, Func updater) where T : class, IDomainObject - { - Guard.NotNull(updater, nameof(updater)); - - return InvokeSyncedAsync(context, updater, true); - } - - private async Task InvokeAsync(CommandContext context, Func handler, bool isUpdate) where T : class, IDomainObject - { - Guard.NotNull(context, nameof(context)); - - var domainCommand = GetCommand(context); - var domainObjectId = domainCommand.AggregateId; - var domainObject = await stateFactory.CreateAsync(domainObjectId); - - if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) - { - throw new DomainObjectVersionException(domainObjectId.ToString(), typeof(T), domainObject.Version, domainCommand.ExpectedVersion); - } - - await handler(domainObject); - - await domainObject.WriteAsync(); - - if (!context.IsCompleted) - { - if (isUpdate) - { - context.Complete(new EntitySavedResult(domainObject.Version)); - } - else - { - context.Complete(EntityCreatedResult.Create(domainObjectId, domainObject.Version)); - } - } - - return domainObject; - } - - private async Task InvokeSyncedAsync(CommandContext context, Func handler, bool isUpdate) where T : class, IDomainObject - { - Guard.NotNull(context, nameof(context)); - - var domainCommand = GetCommand(context); - var domainObjectId = domainCommand.AggregateId; - - using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId))) - { - var domainObject = await stateFactory.GetSingleAsync(domainObjectId); - - if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version) - { - throw new DomainObjectVersionException(domainObjectId.ToString(), typeof(T), domainObject.Version, domainCommand.ExpectedVersion); - } - - await handler(domainObject); - - try - { - await domainObject.WriteAsync(); - - stateFactory.Synchronize(domainObjectId); - } - catch - { - stateFactory.Remove(domainObjectId); - - throw; - } - - if (!context.IsCompleted) - { - if (isUpdate) - { - context.Complete(new EntitySavedResult(domainObject.Version)); - } - else - { - context.Complete(EntityCreatedResult.Create(domainObjectId, domainObject.Version)); - } - } - - return domainObject; - } - } - - private static IAggregateCommand GetCommand(CommandContext context) - { - if (!(context.Command is IAggregateCommand command)) - { - throw new ArgumentException("Context must have an aggregate command.", nameof(context)); - } - - Guard.NotEmpty(command.AggregateId, "context.Command.AggregateId"); - - return command; - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index c2cb8bff0..c669a221a 100644 --- a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Threading.Tasks; using Squidex.Infrastructure.Tasks; @@ -13,26 +12,6 @@ namespace Squidex.Infrastructure.Commands { public static class CommandExtensions { - public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject - { - return handler.CreateAsync(context, creator.ToAsync()); - } - - public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject - { - return handler.UpdateAsync(context, updater.ToAsync()); - } - - public static Task CreateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject - { - return handler.CreateSyncedAsync(context, creator.ToAsync()); - } - - public static Task UpdateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject - { - return handler.UpdateSyncedAsync(context, updater.ToAsync()); - } - public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context) { return commandMiddleware.HandleAsync(context, () => TaskHelper.Done); diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs deleted file mode 100644 index 888c9ff9f..000000000 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class DomainObjectBase : IDomainObject where T : IDomainState, new() - { - private readonly List> uncomittedEvents = new List>(); - private Guid id; - private T snapshot = new T { Version = EtagVersion.Empty }; - private IPersistence persistence; - - public long Version - { - get { return snapshot.Version; } - } - - public T Snapshot - { - get { return snapshot; } - } - - public Task ActivateAsync(Guid key, IStore store) - { - id = key; - - persistence = store.WithSnapshotsAndEventSourcing(key, ApplySnapshot, ApplyEvent); - - return persistence.ReadAsync(); - } - - public void RaiseEvent(IEvent @event) - { - RaiseEvent(Envelope.Create(@event)); - } - - public virtual void RaiseEvent(Envelope @event) - { - Guard.NotNull(@event, nameof(@event)); - - @event.SetAggregateId(id); - - ApplyEvent(@event); - - snapshot.Version++; - - uncomittedEvents.Add(@event); - } - - public IReadOnlyList> GetUncomittedEvents() - { - return uncomittedEvents; - } - - public void ClearUncommittedEvents() - { - uncomittedEvents.Clear(); - } - - public virtual void ApplySnapshot(T newSnapshot) - { - snapshot = newSnapshot; - } - - public virtual void ApplyEvent(Envelope @event) - { - } - - public Task WriteSnapshotAsync() - { - snapshot.Version = persistence.Version; - - return persistence.WriteSnapshotAsync(snapshot); - } - - public async Task WriteAsync() - { - var events = uncomittedEvents.ToArray(); - - if (events.Length > 0) - { - try - { - snapshot.Version = persistence.Version + events.Length; - - await persistence.WriteEventsAsync(events); - await persistence.WriteSnapshotAsync(snapshot); - } - finally - { - uncomittedEvents.Clear(); - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs new file mode 100644 index 000000000..6d6768581 --- /dev/null +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -0,0 +1,204 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public abstract class DomainObjectGrain : IDomainObjectGrain where T : IDomainState, new() + { + private readonly List> uncomittedEvents = new List>(); + private readonly IStore store; + private Guid id; + private T snapshot = new T { Version = EtagVersion.Empty }; + private IPersistence persistence; + + public Guid Id + { + get { return id; } + } + + public long Version + { + get { return snapshot.Version; } + } + + public long NewVersion + { + get { return snapshot.Version + uncomittedEvents.Count; } + } + + public T Snapshot + { + get { return snapshot; } + } + + protected DomainObjectGrain(IStore store) + { + Guard.NotNull(store, nameof(store)); + + this.store = store; + } + + public Task ActivateAsync(Guid key) + { + id = key; + + persistence = store.WithSnapshotsAndEventSourcing(GetType(), key, ApplySnapshot, ApplyEvent); + + return persistence.ReadAsync(); + } + + public void RaiseEvent(IEvent @event) + { + RaiseEvent(Envelope.Create(@event)); + } + + public virtual void RaiseEvent(Envelope @event) + { + Guard.NotNull(@event, nameof(@event)); + + @event.SetAggregateId(Id); + + ApplyEvent(@event); + + uncomittedEvents.Add(@event); + } + + public IReadOnlyList> GetUncomittedEvents() + { + return uncomittedEvents; + } + + public void ClearUncommittedEvents() + { + uncomittedEvents.Clear(); + } + + public virtual void ApplySnapshot(T newSnapshot) + { + snapshot = newSnapshot; + } + + public virtual void ApplyEvent(Envelope @event) + { + } + + public Task WriteSnapshotAsync() + { + snapshot.Version = persistence.Version; + + return persistence.WriteSnapshotAsync(snapshot); + } + + protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler, false); + } + + protected Task CreateReturnAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToAsync(), false); + } + + protected Task CreateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler.ToDefault(), false); + } + + protected Task CreateAsync(TCommand command, Action handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()?.ToAsync(), false); + } + + protected Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler, true); + } + + protected Task UpdateReturnAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToAsync(), true); + } + + protected Task UpdateAsync(TCommand command, Func handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault(), true); + } + + protected Task UpdateAsync(TCommand command, Action handler) where TCommand : class, IAggregateCommand + { + return InvokeAsync(command, handler?.ToDefault()?.ToAsync(), true); + } + + private async Task InvokeAsync(TCommand command, Func> handler, bool isUpdate) where TCommand : class, IAggregateCommand + { + Guard.NotNull(command, nameof(command)); + + if (command.ExpectedVersion != EtagVersion.Any && command.ExpectedVersion != Version) + { + throw new DomainObjectVersionException(Id.ToString(), GetType(), Version, command.ExpectedVersion); + } + + if (isUpdate && Version < 0) + { + throw new DomainObjectNotFoundException(Id.ToString(), GetType()); + } + else if (!isUpdate && Version >= 0) + { + throw new DomainException("Object has already been created."); + } + + var previousSnapshot = snapshot; + try + { + var result = await handler(command); + + var events = uncomittedEvents.ToArray(); + + if (events.Length > 0) + { + snapshot.Version = NewVersion; + + await persistence.WriteEventsAsync(events); + await persistence.WriteSnapshotAsync(snapshot); + } + + if (result == null) + { + if (isUpdate) + { + result = new EntitySavedResult(Version); + } + else + { + result = EntityCreatedResult.Create(Id, Version); + } + } + + return result; + } + catch + { + snapshot = previousSnapshot; + + throw; + } + finally + { + ClearUncommittedEvents(); + } + } + + public abstract Task ExecuteAsync(IAggregateCommand command); + } +} diff --git a/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs new file mode 100644 index 000000000..a6ef41363 --- /dev/null +++ b/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.Commands +{ + public class GrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain + { + private readonly IStateFactory stateFactory; + + public GrainCommandMiddleware(IStateFactory stateFactory) + { + Guard.NotNull(stateFactory, nameof(stateFactory)); + + this.stateFactory = stateFactory; + } + + public async virtual Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is TCommand typedCommand) + { + var result = await ExecuteCommandAsync(typedCommand); + + context.Complete(result); + } + + await next(); + } + + protected async Task ExecuteCommandAsync(TCommand typedCommand) + { + var grain = await stateFactory.CreateAsync(typedCommand.AggregateId); + + var result = await grain.ExecuteAsync(typedCommand); + + return result; + } + } +} diff --git a/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs b/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs deleted file mode 100644 index 7a68f39a6..000000000 --- a/src/Squidex.Infrastructure/Commands/IAggregateHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public interface IAggregateHandler - { - Task CreateAsync(CommandContext context, Func creator) where T : class, IDomainObject; - - Task CreateSyncedAsync(CommandContext context, Func creator) where T : class, IDomainObject; - - Task UpdateAsync(CommandContext context, Func updater) where T : class, IDomainObject; - - Task UpdateSyncedAsync(CommandContext context, Func updater) where T : class, IDomainObject; - } -} diff --git a/src/Squidex.Infrastructure/Commands/IDomainObject.cs b/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs similarity index 69% rename from src/Squidex.Infrastructure/Commands/IDomainObject.cs rename to src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs index 98d4521b1..37c80cdb6 100644 --- a/src/Squidex.Infrastructure/Commands/IDomainObject.cs +++ b/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs @@ -1,7 +1,7 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== @@ -11,10 +11,12 @@ using Squidex.Infrastructure.States; namespace Squidex.Infrastructure.Commands { - public interface IDomainObject : IStatefulObject + public interface IDomainObjectGrain : IStatefulObject { - long Version { get; } + Task ExecuteAsync(IAggregateCommand command); + + Task WriteSnapshotAsync(); - Task WriteAsync(); + long Version { get; } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Commands/SyncedGrainCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/SyncedGrainCommandMiddleware.cs new file mode 100644 index 000000000..d3d85159f --- /dev/null +++ b/src/Squidex.Infrastructure/Commands/SyncedGrainCommandMiddleware.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Commands +{ + public class SyncedGrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain + { + private readonly AsyncLockPool lockPool = new AsyncLockPool(10000); + private readonly IStateFactory stateFactory; + + public SyncedGrainCommandMiddleware(IStateFactory stateFactory) + { + Guard.NotNull(stateFactory, nameof(stateFactory)); + + this.stateFactory = stateFactory; + } + + public async virtual Task HandleAsync(CommandContext context, Func next) + { + if (context.Command is TCommand typedCommand) + { + var result = await ExecuteCommandAsync(typedCommand); + + context.Complete(result); + } + + await next(); + } + + protected async Task ExecuteCommandAsync(TCommand typedCommand) + { + var id = typedCommand.AggregateId; + + using (await lockPool.LockAsync(typedCommand.AggregateId)) + { + try + { + var grain = await stateFactory.GetSingleAsync(id); + + var result = await grain.ExecuteAsync(typedCommand); + + stateFactory.Synchronize(id); + + return result; + } + catch + { + stateFactory.Remove(id); + throw; + } + } + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index 04194ae7f..e05a08c65 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -17,6 +17,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains public class EventConsumerGrain : DisposableObjectBase, IStatefulObject, IEventSubscriber { private readonly IEventDataFormatter eventDataFormatter; + private readonly IStore store; private readonly IEventStore eventStore; private readonly ISemanticLog log; private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(1); @@ -26,16 +27,18 @@ namespace Squidex.Infrastructure.EventSourcing.Grains private EventConsumerState state = new EventConsumerState(); public EventConsumerGrain( + IStore store, IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log) { Guard.NotNull(log, nameof(log)); + Guard.NotNull(store, nameof(store)); Guard.NotNull(eventStore, nameof(eventStore)); Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); this.log = log; - + this.store = store; this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; } @@ -48,9 +51,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } } - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key) { - persistence = store.WithSnapshots(key, s => state = s); + persistence = store.WithSnapshots(key, s => state = s); return persistence.ReadAsync(); } diff --git a/src/Squidex.Infrastructure/States/IStatefulObject.cs b/src/Squidex.Infrastructure/States/IStatefulObject.cs index e90e84728..45769b2ce 100644 --- a/src/Squidex.Infrastructure/States/IStatefulObject.cs +++ b/src/Squidex.Infrastructure/States/IStatefulObject.cs @@ -11,6 +11,6 @@ namespace Squidex.Infrastructure.States { public interface IStatefulObject { - Task ActivateAsync(TKey key, IStore store); + Task ActivateAsync(TKey key); } } diff --git a/src/Squidex.Infrastructure/States/IStore.cs b/src/Squidex.Infrastructure/States/IStore.cs index 7ac2c6dea..1ba437fde 100644 --- a/src/Squidex.Infrastructure/States/IStore.cs +++ b/src/Squidex.Infrastructure/States/IStore.cs @@ -13,10 +13,10 @@ namespace Squidex.Infrastructure.States { public interface IStore { - IPersistence WithEventSourcing(TKey key, Func, Task> applyEvent); + IPersistence WithEventSourcing(Type owner, TKey key, Func, Task> applyEvent); - IPersistence WithSnapshots(TKey key, Func applySnapshot); + IPersistence WithSnapshots(Type owner, TKey key, Func applySnapshot); - IPersistence WithSnapshotsAndEventSourcing(TKey key, Func applySnapshot, Func, Task> applyEvent); + IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, Func applySnapshot, Func, Task> applyEvent); } } diff --git a/src/Squidex.Infrastructure/States/Persistence.cs b/src/Squidex.Infrastructure/States/Persistence.cs index 92dd38738..5c0d79037 100644 --- a/src/Squidex.Infrastructure/States/Persistence.cs +++ b/src/Squidex.Infrastructure/States/Persistence.cs @@ -11,15 +11,15 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - internal sealed class Persistence : Persistence, IPersistence + internal sealed class Persistence : Persistence, IPersistence { - public Persistence(TKey ownerKey, + public Persistence(TKey ownerKey, Type ownerType, IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISnapshotStore snapshotStore, IStreamNameResolver streamNameResolver, Func, Task> applyEvent) - : base(ownerKey, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) + : base(ownerKey, ownerType, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) { } } diff --git a/src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs b/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs similarity index 93% rename from src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs rename to src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs index cb0900aed..ea8b50d1e 100644 --- a/src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs +++ b/src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs @@ -15,9 +15,10 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - internal class Persistence : IPersistence + internal class Persistence : IPersistence { private readonly TKey ownerKey; + private readonly Type ownerType; private readonly ISnapshotStore snapshotStore; private readonly IStreamNameResolver streamNameResolver; private readonly IEventStore eventStore; @@ -34,7 +35,7 @@ namespace Squidex.Infrastructure.States get { return version; } } - public Persistence(TKey ownerKey, + public Persistence(TKey ownerKey, Type ownerType, IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISnapshotStore snapshotStore, @@ -44,6 +45,7 @@ namespace Squidex.Infrastructure.States Func, Task> applyEvent) { this.ownerKey = ownerKey; + this.ownerType = ownerType; this.applyState = applyState; this.applyEvent = applyEvent; this.eventStore = eventStore; @@ -67,11 +69,11 @@ namespace Squidex.Infrastructure.States { if (version == EtagVersion.Empty) { - throw new DomainObjectNotFoundException(ownerKey.ToString(), typeof(TOwner)); + throw new DomainObjectNotFoundException(ownerKey.ToString(), ownerType); } else { - throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), version, expectedVersion); + throw new DomainObjectVersionException(ownerKey.ToString(), ownerType, version, expectedVersion); } } } @@ -134,7 +136,7 @@ namespace Squidex.Infrastructure.States } catch (InconsistentStateException ex) { - throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + throw new DomainObjectVersionException(ownerKey.ToString(), ownerType, ex.CurrentVersion, ex.ExpectedVersion); } versionSnapshot = newVersion; @@ -164,7 +166,7 @@ namespace Squidex.Infrastructure.States } catch (WrongEventVersionException ex) { - throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion); + throw new DomainObjectVersionException(ownerKey.ToString(), ownerType, ex.CurrentVersion, ex.ExpectedVersion); } versionEvents += eventArray.Length; @@ -180,7 +182,7 @@ namespace Squidex.Infrastructure.States private string GetStreamName() { - return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey.ToString()); + return streamNameResolver.GetStreamName(ownerType, ownerKey.ToString()); } private bool UseSnapshots() diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs index 85b9977b4..6e196beba 100644 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ b/src/Squidex.Infrastructure/States/StateFactory.cs @@ -8,7 +8,6 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -using Squidex.Infrastructure.EventSourcing; #pragma warning disable RECS0096 // Type parameter is never used @@ -20,9 +19,6 @@ namespace Squidex.Infrastructure.States private readonly IPubSub pubSub; private readonly IMemoryCache statesCache; private readonly IServiceProvider services; - private readonly IStreamNameResolver streamNameResolver; - private readonly IEventStore eventStore; - private readonly IEventDataFormatter eventDataFormatter; private readonly object lockObject = new object(); private IDisposable pubSubSubscription; @@ -31,11 +27,11 @@ namespace Squidex.Infrastructure.States private readonly Task activationTask; private readonly T obj; - public ObjectHolder(T obj, TKey key, IStore store) + public ObjectHolder(T obj, TKey key) { this.obj = obj; - activationTask = obj.ActivateAsync(key, store); + activationTask = obj.ActivateAsync(key); } public async Task ActivateAsync() @@ -46,27 +42,15 @@ namespace Squidex.Infrastructure.States } } - public StateFactory( - IPubSub pubSub, - IMemoryCache statesCache, - IEventStore eventStore, - IEventDataFormatter eventDataFormatter, - IServiceProvider services, - IStreamNameResolver streamNameResolver) + public StateFactory(IPubSub pubSub, IMemoryCache statesCache, IServiceProvider services) { - Guard.NotNull(services, nameof(services)); - Guard.NotNull(eventStore, nameof(eventStore)); - Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); Guard.NotNull(pubSub, nameof(pubSub)); + Guard.NotNull(services, nameof(services)); Guard.NotNull(statesCache, nameof(statesCache)); - Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); - this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; this.pubSub = pubSub; this.services = services; this.statesCache = statesCache; - this.streamNameResolver = streamNameResolver; } public void Initialize() @@ -94,10 +78,9 @@ namespace Squidex.Infrastructure.States { Guard.NotNull(key, nameof(key)); - var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver); var state = (T)services.GetService(typeof(T)); - await state.ActivateAsync(key, stateStore); + await state.ActivateAsync(key); return state; } @@ -124,9 +107,8 @@ namespace Squidex.Infrastructure.States } var state = (T)services.GetService(typeof(T)); - var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver); - stateObj = new ObjectHolder(state, key, stateStore); + stateObj = new ObjectHolder(state, key); statesCache.CreateEntry(key) .SetValue(stateObj) diff --git a/src/Squidex.Infrastructure/States/Store.cs b/src/Squidex.Infrastructure/States/Store.cs index 2d1daa69c..a3d6b1bc6 100644 --- a/src/Squidex.Infrastructure/States/Store.cs +++ b/src/Squidex.Infrastructure/States/Store.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - internal sealed class Store : IStore + public sealed class Store : IStore { private readonly IServiceProvider services; private readonly IStreamNameResolver streamNameResolver; @@ -30,32 +30,32 @@ namespace Squidex.Infrastructure.States this.streamNameResolver = streamNameResolver; } - public IPersistence WithSnapshots(TKey key, Func applySnapshot) + public IPersistence WithSnapshots(Type owner, TKey key, Func applySnapshot) { - return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null); + return CreatePersistence(owner, key, PersistenceMode.Snapshots, applySnapshot, null); } - public IPersistence WithSnapshotsAndEventSourcing(TKey key, Func applySnapshot, Func, Task> applyEvent) + public IPersistence WithSnapshotsAndEventSourcing(Type owner, TKey key, Func applySnapshot, Func, Task> applyEvent) { - return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); + return CreatePersistence(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent); } - public IPersistence WithEventSourcing(TKey key, Func, Task> applyEvent) + public IPersistence WithEventSourcing(Type owner, TKey key, Func, Task> applyEvent) { Guard.NotDefault(key, nameof(key)); var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - return new Persistence(key, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); + return new Persistence(key, owner, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); } - private IPersistence CreatePersistence(TKey key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) + private IPersistence CreatePersistence(Type owner, TKey key, PersistenceMode mode, Func applySnapshot, Func, Task> applyEvent) { Guard.NotDefault(key, nameof(key)); var snapshotStore = (ISnapshotStore)services.GetService(typeof(ISnapshotStore)); - return new Persistence(key, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); + return new Persistence(key, owner, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); } } } diff --git a/src/Squidex.Infrastructure/States/StoreExtensions.cs b/src/Squidex.Infrastructure/States/StoreExtensions.cs index 5a4dec34b..3cee24593 100644 --- a/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Tasks; @@ -13,19 +14,49 @@ namespace Squidex.Infrastructure.States { public static class StoreExtensions { - public static IPersistence WithEventSourcing(this IStore store, TKey key, Action> applyEvent) + public static IPersistence WithEventSourcing(this IStore store, TKey key, Func, Task> applyEvent) { - return store.WithEventSourcing(key, applyEvent.ToAsync()); + return store.WithEventSourcing(typeof(TOwner), key, applyEvent); } - public static IPersistence WithSnapshots(this IStore store, TKey key, Action applySnapshot) + public static IPersistence WithSnapshots(this IStore store, TKey key, Func applySnapshot) { - return store.WithSnapshots(key, applySnapshot.ToAsync()); + return store.WithSnapshots(typeof(TOwner), key, applySnapshot); } - public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Action applySnapshot, Action> applyEvent) + public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Func applySnapshot, Func, Task> applyEvent) { - return store.WithSnapshotsAndEventSourcing(key, applySnapshot.ToAsync(), applyEvent.ToAsync()); + return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot, applyEvent); + } + + public static IPersistence WithEventSourcing(this IStore store, Type owner, TKey key, Action> applyEvent) + { + return store.WithEventSourcing(owner, key, applyEvent.ToAsync()); + } + + public static IPersistence WithSnapshots(this IStore store, Type owner, TKey key, Action applySnapshot) + { + return store.WithSnapshots(owner, key, applySnapshot.ToAsync()); + } + + public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, Type owner, TKey key, Action applySnapshot, Action> applyEvent) + { + return store.WithSnapshotsAndEventSourcing(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync()); + } + + public static IPersistence WithEventSourcing(this IStore store, TKey key, Action> applyEvent) + { + return store.WithEventSourcing(typeof(TOwner), key, applyEvent.ToAsync()); + } + + public static IPersistence WithSnapshots(this IStore store, TKey key, Action applySnapshot) + { + return store.WithSnapshots(typeof(TOwner), key, applySnapshot.ToAsync()); + } + + public static IPersistence WithSnapshotsAndEventSourcing(this IStore store, TKey key, Action applySnapshot, Action> applyEvent) + { + return store.WithSnapshotsAndEventSourcing(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync()); } } } diff --git a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs index f6bca06ce..8d5f8a548 100644 --- a/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ b/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -16,7 +16,43 @@ namespace Squidex.Infrastructure.Tasks { } - public static Func ToAsync(this Action action) + public static Func ToDefault(this Action action) + { + Guard.NotNull(action, nameof(action)); + + return x => + { + action(x); + + return default(TOutput); + }; + } + + public static Func> ToDefault(this Func action) + { + Guard.NotNull(action, nameof(action)); + + return async x => + { + await action(x); + + return default(TOutput); + }; + } + + public static Func> ToAsync(this Func action) + { + Guard.NotNull(action, nameof(action)); + + return x => + { + var result = action(x); + + return Task.FromResult(result); + }; + } + + public static Func ToAsync(this Action action) { return x => { diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 9a182bf87..c8a820086 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -77,9 +77,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index 505a0d4e8..d77e4588a 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/WriteServices.cs @@ -12,11 +12,15 @@ using Migrate_01; using Migrate_01.Migrations; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Users; using Squidex.Infrastructure.Commands; @@ -50,19 +54,19 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); services.AddSingletonAs() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); services.AddSingletonAs() @@ -92,19 +96,19 @@ namespace Squidex.Config.Domain services.AddTransientAs() .AsSelf(); - services.AddTransientAs() + services.AddTransientAs() .AsSelf(); - services.AddTransientAs() + services.AddTransientAs() .AsSelf(); - services.AddTransientAs() + services.AddTransientAs() .AsSelf(); - services.AddTransientAs() + services.AddTransientAs() .AsSelf(); - services.AddTransientAs() + services.AddTransientAs() .AsSelf(); services.AddSingleton(c => diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs deleted file mode 100644 index 18ab306fa..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ /dev/null @@ -1,291 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.States; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppCommandMiddlewareTests : HandlerTestBase - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly Language language = Language.DE; - private readonly string contributorId = Guid.NewGuid().ToString(); - private readonly string clientName = "client"; - private readonly Guid patternId = Guid.NewGuid(); - private readonly AppDomainObject app = new AppDomainObject(new InitialPatterns()); - private readonly AppCommandMiddleware sut; - - protected override Guid Id - { - get { return AppId; } - } - - public AppCommandMiddlewareTests() - { - A.CallTo(() => appProvider.GetAppAsync(AppName)) - .Returns((IAppEntity)null); - - A.CallTo(() => userResolver.FindByIdAsync(contributorId)) - .Returns(A.Fake()); - - sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver); - - app.ActivateAsync(Id, A.Fake>()); - } - - [Fact] - public async Task Create_should_create_domain_object() - { - var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId }); - - await TestCreate(app, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(AppId, context.Result>().IdOrValue); - } - - [Fact] - public async Task AssignContributor_should_update_domain_object_if_user_found() - { - A.CallTo(() => appPlansProvider.GetPlan(null)) - .Returns(new ConfigAppLimitsPlan { MaxContributors = -1 }); - - CreateApp(); - - var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RemoveContributor_should_update_domain_object() - { - CreateApp() - .AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); - - var context = CreateContextForCommand(new RemoveContributor { ContributorId = contributorId }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task AttachClient_should_update_domain_object() - { - CreateApp(); - - var context = CreateContextForCommand(new AttachClient { Id = clientName }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RenameClient_should_update_domain_object() - { - CreateApp() - .AttachClient(CreateCommand(new AttachClient { Id = clientName })); - - var context = CreateContextForCommand(new UpdateClient { Id = clientName, Name = "New Name" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RevokeClient_should_update_domain_object() - { - CreateApp() - .AttachClient(CreateCommand(new AttachClient { Id = clientName })); - - var context = CreateContextForCommand(new RevokeClient { Id = clientName }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task ChangePlan_should_update_domain_object() - { - A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) - .Returns(true); - - CreateApp(); - - var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) - .MustHaveHappened(); - } - - [Fact] - public async Task ChangePlan_should_not_make_update_for_redirect_result() - { - A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) - .Returns(true); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) - .Returns(CreateRedirectResult()); - - CreateApp(); - - var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Null(app.Snapshot.Plan); - } - - [Fact] - public async Task ChangePlan_should_not_call_billing_manager_for_callback() - { - A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) - .Returns(true); - - CreateApp(); - - var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan", FromCallback = true }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) - .MustNotHaveHappened(); - } - - [Fact] - public async Task AddLanguage_should_update_domain_object() - { - CreateApp(); - - var context = CreateContextForCommand(new AddLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RemoveLanguage_should_update_domain_object() - { - CreateApp() - .AddLanguage(CreateCommand(new AddLanguage { Language = language })); - - var context = CreateContextForCommand(new RemoveLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task UpdateLanguage_should_update_domain_object() - { - CreateApp() - .AddLanguage(CreateCommand(new AddLanguage { Language = language })); - - var context = CreateContextForCommand(new UpdateLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task AddPattern_should_update_domain_object() - { - CreateApp(); - - var context = CreateContextForCommand(new AddPattern { Name = "Any", Pattern = ".*" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task UpdatePattern_should_update_domain() - { - CreateApp() - .AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = "." })); - - var context = CreateContextForCommand(new UpdatePattern { PatternId = patternId, Name = "Number", Pattern = "[0-9]" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task DeletePattern_should_update_domain_object() - { - CreateApp() - .AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = "." })); - - var context = CreateContextForCommand(new DeletePattern { PatternId = patternId }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - private AppDomainObject CreateApp() - { - app.Create(CreateCommand(new CreateApp { AppId = AppId, Name = AppName })); - - return app; - } - - private static Task CreateRedirectResult() - { - return Task.FromResult(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs deleted file mode 100644 index e49cf171e..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ /dev/null @@ -1,399 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public class AppDomainObjectTests : HandlerTestBase - { - private readonly string contributorId = Guid.NewGuid().ToString(); - private readonly string clientId = "client"; - private readonly string clientNewName = "My Client"; - private readonly string planId = "premium"; - private readonly Guid patternId = Guid.NewGuid(); - private readonly AppDomainObject sut = new AppDomainObject(new InitialPatterns()); - - protected override Guid Id - { - get { return AppId; } - } - - public AppDomainObjectTests() - { - sut.ActivateAsync(Id, A.Fake>()); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - CreateApp(); - - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateApp { Name = AppName })); - }); - } - - [Fact] - public void Create_should_specify_name_and_owner() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var initialPatterns = new InitialPatterns - { - { id1, new AppPattern("Number", "[0-9]") }, - { id2, new AppPattern("Numbers", "[0-9]*") } - }; - - var app = new AppDomainObject(initialPatterns); - - app.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId })); - - Assert.Equal(AppName, app.Snapshot.Name); - - app.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppCreated { Name = AppName }), - CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), - CreateEvent(new AppLanguageAdded { Language = Language.EN }), - CreateEvent(new AppPatternAdded { PatternId = id1, Name = "Number", Pattern = "[0-9]" }), - CreateEvent(new AppPatternAdded { PatternId = id2, Name = "Numbers", Pattern = "[0-9]*" }) - ); - } - - [Fact] - public void ChangePlan_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - }); - } - - [Fact] - public void ChangePlan_should_create_events() - { - CreateApp(); - - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - - Assert.Equal(planId, sut.Snapshot.Plan.PlanId); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppPlanChanged { PlanId = planId }) - ); - } - - [Fact] - public void AssignContributor_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); - }); - } - - [Fact] - public void AssignContributor_should_create_events() - { - CreateApp(); - - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); - - Assert.Equal(AppContributorPermission.Editor, sut.Snapshot.Contributors[contributorId]); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) - ); - } - - [Fact] - public void RemoveContributor_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); - }); - } - - [Fact] - public void RemoveContributor_should_create_events_and_remove_contributor() - { - CreateApp(); - - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); - sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); - - Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); - - sut.GetUncomittedEvents().Skip(1) - .ShouldHaveSameEvents( - CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) - ); - } - - [Fact] - public void AttachClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); - }); - } - - [Fact] - public void AttachClient_should_create_events() - { - var command = new AttachClient { Id = clientId }; - - CreateApp(); - - sut.AttachClient(CreateCommand(command)); - - Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) - ); - } - - [Fact] - public void RevokeClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RevokeClient(CreateCommand(new RevokeClient { Id = "not-found" })); - }); - } - - [Fact] - public void RevokeClient_should_create_events() - { - CreateApp(); - CreateClient(); - - sut.RevokeClient(CreateCommand(new RevokeClient { Id = clientId })); - - Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientRevoked { Id = clientId }) - ); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); - }); - } - - [Fact] - public void UpdateClient_should_create_events() - { - CreateApp(); - CreateClient(); - - sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer })); - - Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), - CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) - ); - } - - [Fact] - public void AddLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); - }); - } - - [Fact] - public void AddLanguage_should_create_events() - { - CreateApp(); - - sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); - - Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageAdded { Language = Language.DE }) - ); - } - - [Fact] - public void RemoveLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.EN })); - }); - } - - [Fact] - public void RemoveLanguage_should_create_events() - { - CreateApp(); - CreateLanguage(Language.DE); - - sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); - - Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageRemoved { Language = Language.DE }) - ); - } - - [Fact] - public void UpdateLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN })); - }); - } - - [Fact] - public void UpdateLanguage_should_create_events() - { - CreateApp(); - CreateLanguage(Language.DE); - - sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } })); - - Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) - ); - } - - [Fact] - public void AddPattern_should_throw_exception_if_app_not_created() - { - Assert.Throws(() => sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = ".*" }))); - } - - [Fact] - public void AddPattern_should_create_events() - { - CreateApp(); - - sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" })); - - Assert.Single(sut.Snapshot.Patterns); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppPatternAdded { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" }) - ); - } - - [Fact] - public void DeletePattern_should_throw_exception_if_app_not_created() - { - Assert.Throws(() => - { - sut.DeletePattern(CreateCommand(new DeletePattern - { - PatternId = Guid.NewGuid() - })); - }); - } - - [Fact] - public void DeletePattern_should_create_events() - { - CreateApp(); - CreatePattern(); - - sut.DeletePattern(CreateCommand(new DeletePattern { PatternId = patternId })); - - Assert.Empty(sut.Snapshot.Patterns); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppPatternDeleted { PatternId = patternId }) - ); - } - - [Fact] - public void UpdatePattern_should_throw_exception_if_app_not_created() - { - Assert.Throws(() => sut.UpdatePattern(CreateCommand(new UpdatePattern { PatternId = patternId, Name = "Any", Pattern = ".*" }))); - } - - [Fact] - public void UpdatePattern_should_create_events() - { - CreateApp(); - CreatePattern(); - - sut.UpdatePattern(CreateCommand(new UpdatePattern { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" })); - - Assert.Single(sut.Snapshot.Patterns); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppPatternUpdated { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" }) - ); - } - - private void CreatePattern() - { - sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Name", Pattern = ".*" })); - sut.ClearUncommittedEvents(); - } - - private void CreateApp() - { - sut.Create(CreateCommand(new CreateApp { Name = AppName })); - sut.ClearUncommittedEvents(); - } - - private void CreateClient() - { - sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); - sut.ClearUncommittedEvents(); - } - - private void CreateLanguage(Language language) - { - sut.AddLanguage(CreateCommand(new AddLanguage { Language = language })); - sut.ClearUncommittedEvents(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs new file mode 100644 index 000000000..a741e778f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -0,0 +1,388 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppGrainTests : HandlerTestBase + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAppPlansProvider appPlansProvider = A.Fake(); + private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly string contributorId = Guid.NewGuid().ToString(); + private readonly string clientId = "client"; + private readonly string clientNewName = "My Client"; + private readonly string planId = "premium"; + private readonly AppGrain sut; + private readonly Guid patternId1 = Guid.NewGuid(); + private readonly Guid patternId2 = Guid.NewGuid(); + private readonly Guid patternId3 = Guid.NewGuid(); + private readonly InitialPatterns initialPatterns; + + protected override Guid Id + { + get { return AppId; } + } + + public AppGrainTests() + { + A.CallTo(() => appProvider.GetAppAsync(AppName)) + .Returns((IAppEntity)null); + + A.CallTo(() => userResolver.FindByIdAsync(contributorId)) + .Returns(A.Fake()); + + initialPatterns = new InitialPatterns + { + { patternId1, new AppPattern("Number", "[0-9]") }, + { patternId2, new AppPattern("Numbers", "[0-9]*") } + }; + + sut = new AppGrain(initialPatterns, Store, appProvider, appPlansProvider, appPlansBillingManager, userResolver); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public async Task Create_should_create_events_and_update_state() + { + var command = new CreateApp { Name = AppName, Actor = User, AppId = AppId }; + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 4)); + + Assert.Equal(AppName, sut.Snapshot.Name); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppCreated { Name = AppName }), + CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), + CreateEvent(new AppLanguageAdded { Language = Language.EN }), + CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }), + CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" }) + ); + } + + [Fact] + public async Task ChangePlan_should_create_events_and_update_state() + { + var command = new ChangePlan { PlanId = planId }; + + A.CallTo(() => appPlansProvider.IsConfiguredPlan(planId)) + .Returns(true); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planId)) + .Returns(new PlanChangedResult()); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + Assert.True(result is PlanChangedResult); + + Assert.Equal(planId, sut.Snapshot.Plan.PlanId); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPlanChanged { PlanId = planId }) + ); + } + + [Fact] + public async Task ChangePlan_should_not_make_update_for_redirect_result() + { + var command = new ChangePlan { PlanId = planId }; + + A.CallTo(() => appPlansProvider.IsConfiguredPlan(planId)) + .Returns(true); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planId)) + .Returns(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + + Assert.Null(sut.Snapshot.Plan); + } + + [Fact] + public async Task ChangePlan_should_not_call_billing_manager_for_callback() + { + var command = new ChangePlan { PlanId = planId, FromCallback = true }; + + A.CallTo(() => appPlansProvider.IsConfiguredPlan(planId)) + .Returns(true); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task AssignContributor_should_create_events_and_update_state() + { + var command = new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + Assert.Equal(AppContributorPermission.Editor, sut.Snapshot.Contributors[contributorId]); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) + ); + } + + [Fact] + public async Task RemoveContributor_should_create_events_and_update_state() + { + var command = new RemoveContributor { ContributorId = contributorId }; + + await ExecuteCreateAsync(); + await ExecuteAssignContributorAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(6)); + + Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) + ); + } + + [Fact] + public async Task AttachClient_should_create_events_and_update_state() + { + var command = new AttachClient { Id = clientId }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + Assert.True(sut.Snapshot.Clients.ContainsKey(clientId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) + ); + } + + [Fact] + public async Task RevokeClient_should_create_events_and_update_state() + { + var command = new RevokeClient { Id = clientId }; + + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(6)); + + Assert.False(sut.Snapshot.Clients.ContainsKey(clientId)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientRevoked { Id = clientId }) + ); + } + + [Fact] + public async Task UpdateClient_should_create_events_and_update_state() + { + var command = new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer }; + + await ExecuteCreateAsync(); + await ExecuteAttachClientAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(7)); + + Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), + CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) + ); + } + + [Fact] + public async Task AddLanguage_should_create_events_and_update_state() + { + var command = new AddLanguage { Language = Language.DE }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageAdded { Language = Language.DE }) + ); + } + + [Fact] + public async Task RemoveLanguage_should_create_events_and_update_state() + { + var command = new RemoveLanguage { Language = Language.DE }; + + await ExecuteCreateAsync(); + await ExecuteAddLanguageAsync(Language.DE); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(6)); + + Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageRemoved { Language = Language.DE }) + ); + } + + [Fact] + public async Task UpdateLanguage_should_create_events_and_update_state() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; + + await ExecuteCreateAsync(); + await ExecuteAddLanguageAsync(Language.DE); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(6)); + + Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) + ); + } + + [Fact] + public async Task AddPattern_should_create_events_and_update_state() + { + var command = new AddPattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(5)); + + Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternAdded { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) + ); + } + + [Fact] + public async Task DeletePattern_should_create_events_and_update_state() + { + var command = new DeletePattern { PatternId = patternId3 }; + + await ExecuteCreateAsync(); + await ExecuteAddPatternAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(6)); + + Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternDeleted { PatternId = patternId3 }) + ); + } + + [Fact] + public async Task UpdatePattern_should_create_events_and_update_state() + { + var command = new UpdatePattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }; + + await ExecuteCreateAsync(); + await ExecuteAddPatternAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(6)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppPatternUpdated { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" }) + ); + } + + private Task ExecuteAddPatternAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" })); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateCommand(new CreateApp { Name = AppName })); + } + + private Task ExecuteAssignContributorAsync() + { + return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); + } + + private Task ExecuteAttachClientAsync() + { + return sut.ExecuteAsync(CreateCommand(new AttachClient { Id = clientId })); + } + + private Task ExecuteAddLanguageAsync(Language language) + { + return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language })); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs index 714a22709..99dd68dab 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps.Billing { public class ConfigAppLimitsProviderTests { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 599eaa005..634514689 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -10,22 +10,25 @@ using System.IO; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetCommandMiddlewareTests : HandlerTestBase + public class AssetCommandMiddlewareTests : HandlerTestBase { private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); private readonly IAssetStore assetStore = A.Fake(); + private readonly IStateFactory stateFactory = 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 AssetGrain asset; private readonly AssetFile file; private readonly AssetCommandMiddleware sut; @@ -38,7 +41,13 @@ namespace Squidex.Domain.Apps.Entities.Assets { file = new AssetFile("my-image.png", "image/png", 1024, () => stream); - sut = new AssetCommandMiddleware(Handler, assetStore, assetThumbnailGenerator); + asset = new AssetGrain(Store); + asset.ActivateAsync(Id).Wait(); + + A.CallTo(() => stateFactory.CreateAsync(Id)) + .Returns(asset); + + sut = new AssetCommandMiddleware(stateFactory, assetStore, assetThumbnailGenerator); } [Fact] @@ -49,10 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets SetupStore(0, context.ContextId); SetupImageInfo(); - await TestCreate(asset, async _ => - { - await sut.HandleAsync(context); - }); + await sut.HandleAsync(context); Assert.Equal(assetId, context.Result>().IdOrValue); @@ -68,52 +74,17 @@ namespace Squidex.Domain.Apps.Entities.Assets SetupStore(1, context.ContextId); SetupImageInfo(); - CreateAsset(); + await ExecuteCreateAsync(); - await TestUpdate(asset, async _ => - { - await sut.HandleAsync(context); - }); + 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() + private Task ExecuteCreateAsync() { - 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); + return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file })); } private void SetupStore(long version, Guid commitId) @@ -126,12 +97,6 @@ namespace Squidex.Domain.Apps.Entities.Assets .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)) @@ -141,5 +106,17 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())) .MustHaveHappened(); } + + private void SetupImageInfo() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(image); + } + + private void AssertAssetImageChecked() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .MustHaveHappened(); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs deleted file mode 100644 index b1c057749..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs +++ /dev/null @@ -1,220 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.IO; -using FakeItEasy; -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 Squidex.Infrastructure.States; -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(); - - protected override Guid Id - { - get { return assetId; } - } - - public AssetDomainObjectTests() - { - sut.ActivateAsync(Id, A.Fake>()); - } - - [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.Snapshot.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.Snapshot.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.Snapshot.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.Snapshot.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 : AssetCommand - { - command.AssetId = assetId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs new file mode 100644 index 000000000..08f58bb11 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs @@ -0,0 +1,170 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Domain.Apps.Entities.Assets.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetGrainTests : 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 AssetGrain sut; + + protected override Guid Id + { + get { return assetId; } + } + + public AssetGrainTests() + { + sut = new AssetGrain(Store); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public async Task Command_should_throw_exception_if_rule_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await Assert.ThrowsAsync(ExecuteUpdateAsync); + } + + [Fact] + public async Task Create_should_create_events() + { + var command = new CreateAsset { File = file, ImageInfo = image }; + + var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + + result.ShouldBeEquivalent(new AssetSavedResult(0, 0)); + + Assert.Equal(0, sut.Snapshot.FileVersion); + + LastEvents + .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 async Task Update_should_create_events() + { + var command = new UpdateAsset { File = file, ImageInfo = image }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + + result.ShouldBeEquivalent(new AssetSavedResult(1, 1)); + + Assert.Equal(1, sut.Snapshot.FileVersion); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetUpdated + { + IsImage = true, + FileSize = file.FileSize, + FileVersion = 1, + MimeType = file.MimeType, + PixelWidth = image.PixelWidth, + PixelHeight = image.PixelHeight + }) + ); + } + + [Fact] + public async Task Rename_should_create_events() + { + var command = new RenameAsset { FileName = "my-new-image.png" }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.Equal("my-new-image.png", sut.Snapshot.FileName); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" }) + ); + } + + [Fact] + public async Task Delete_should_create_events_with_total_file_size() + { + var command = new DeleteAsset(); + + await ExecuteCreateAsync(); + await ExecuteUpdateAsync(); + + var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(2)); + + Assert.True(sut.Snapshot.IsDeleted); + + LastEvents + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 }) + ); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateAssetCommand(new CreateAsset { File = file })); + } + + private Task ExecuteUpdateAsync() + { + return sut.ExecuteAsync(CreateAssetCommand(new UpdateAsset { File = file })); + } + + private Task ExecuteDeleteAsync() + { + return sut.ExecuteAsync(CreateAssetCommand(new DeleteAsset())); + } + + protected T CreateAssetEvent(T @event) where T : AssetEvent + { + @event.AssetId = assetId; + + return CreateEvent(@event); + } + + protected T CreateAssetCommand(T command) where T : AssetCommand + { + command.AssetId = assetId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs deleted file mode 100644 index 113b574da..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs +++ /dev/null @@ -1,264 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using NodaTime; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets.Repositories; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentCommandMiddlewareTests : HandlerTestBase - { - private readonly ISchemaEntity schema = A.Fake(); - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IAppProvider appProvider = A.Fake(); - private readonly IAppEntity app = A.Fake(); - private readonly ClaimsPrincipal user = new ClaimsPrincipal(); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); - private readonly Guid contentId = Guid.NewGuid(); - private readonly ContentDomainObject content = new ContentDomainObject(); - private readonly ContentCommandMiddleware sut; - - protected override Guid Id - { - get { return contentId; } - } - - private readonly NamedContentData invalidData = - new NamedContentData() - .AddField("my-field1", new ContentFieldData() - .AddValue(null)) - .AddField("my-field2", new ContentFieldData() - .AddValue(1)); - private readonly NamedContentData data = - new NamedContentData() - .AddField("my-field1", new ContentFieldData() - .AddValue(1)) - .AddField("my-field2", new ContentFieldData() - .AddValue(1)); - private readonly NamedContentData patch = - new NamedContentData() - .AddField("my-field1", new ContentFieldData() - .AddValue(1)); - - public ContentCommandMiddlewareTests() - { - var schemaDef = - new Schema("my-schema") - .AddField(new NumberField(1, "my-field1", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = true })) - .AddField(new NumberField(2, "my-field2", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = false })); - - sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy(), scriptEngine, A.Dummy()); - - A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); - - A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app); - - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - A.CallTo(() => schema.ScriptCreate).Returns(""); - A.CallTo(() => schema.ScriptChange).Returns(""); - A.CallTo(() => schema.ScriptUpdate).Returns(""); - A.CallTo(() => schema.ScriptDelete).Returns(""); - - A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)).Returns((app, schema)); - } - - [Fact] - public async Task Create_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(invalidData); - - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestCreate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Create_should_create_content() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user }); - - await TestCreate(content, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(data, context.Result>().IdOrValue); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustNotHaveHappened(); - } - - [Fact] - public async Task Create_should_also_invoke_publish_script_when_publishing() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user, Publish = true }); - - await TestCreate(content, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(data, context.Result>().IdOrValue); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task Update_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(invalidData); - - CreateContent(); - - var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestUpdate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - CreateContent(); - - var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data, User = user }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(data, context.Result().Data); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task Patch_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(invalidData); - - CreateContent(); - - var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestUpdate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Patch_should_update_domain_object() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)).Returns(patch); - - CreateContent(); - - var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = patch, User = user }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.NotNull(context.Result().Data); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_publish_domain_object() - { - CreateContent(); - - var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task ChangeStatus_should_not_invoke_scripts_when_scheduled() - { - CreateContent(); - - var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published, DueTime = Instant.MaxValue }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustNotHaveHappened(); - } - - [Fact] - public async Task Delete_should_update_domain_object() - { - CreateContent(); - - var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user }); - - await TestUpdate(content, async _ => - { - await sut.HandleAsync(command); - }); - - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); - } - - private void CreateContent() - { - content.Create(CreateCommand(new CreateContent { Data = data })); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs deleted file mode 100644 index 59e7e0213..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ /dev/null @@ -1,304 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using FakeItEasy; -using FluentAssertions; -using NodaTime; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentDomainObjectTests : HandlerTestBase - { - private readonly NamedContentData data = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)); - private readonly NamedContentData otherData = - new NamedContentData() - .AddField("field2", - new ContentFieldData() - .AddValue("iv", 2)); - private readonly NamedContentData patched; - private readonly Guid contentId = Guid.NewGuid(); - private readonly ContentDomainObject sut = new ContentDomainObject(); - - protected override Guid Id - { - get { return contentId; } - } - - public ContentDomainObjectTests() - { - patched = otherData.MergeInto(data); - - sut.ActivateAsync(Id, A.Fake>()); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(CreateCommand(new CreateContent { Data = data })); - - Assert.Throws(() => - { - sut.Create(CreateContentCommand(new CreateContent { Data = data })); - }); - } - - [Fact] - public void Create_should_create_events() - { - sut.Create(CreateContentCommand(new CreateContent { Data = data })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data }) - ); - } - - [Fact] - public void Create_should_also_publish_if_set_to_true() - { - sut.Create(CreateContentCommand(new CreateContent { Data = data, Publish = true })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentCreated { Data = data }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) - ); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateContentCommand(new UpdateContent { Data = data })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Update(CreateContentCommand(new UpdateContent())); - }); - } - - [Fact] - public void Update_should_create_events() - { - CreateContent(); - - sut.Update(CreateContentCommand(new UpdateContent { Data = otherData })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); - } - - [Fact] - public void Update_should_not_create_event_for_same_data() - { - CreateContent(); - UpdateContent(); - - sut.Update(CreateContentCommand(new UpdateContent { Data = data })); - - sut.GetUncomittedEvents().Should().BeEmpty(); - } - - [Fact] - public void Patch_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Patch(CreateContentCommand(new PatchContent { Data = data })); - }); - } - - [Fact] - public void Patch_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Patch(CreateContentCommand(new PatchContent())); - }); - } - - [Fact] - public void Patch_should_create_events() - { - CreateContent(); - UpdateContent(); - - sut.Patch(CreateContentCommand(new PatchContent { Data = otherData })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = patched }) - ); - } - - [Fact] - public void Patch_should_not_create_event_for_same_data() - { - CreateContent(); - UpdateContent(); - - sut.Patch(CreateContentCommand(new PatchContent { Data = data })); - - sut.GetUncomittedEvents().Should().BeEmpty(); - } - - [Fact] - public void ChangeStatus_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); - }); - } - - [Fact] - public void ChangeStatus_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); - }); - } - - [Fact] - public void ChangeStatus_should_refresh_properties_and_create_events() - { - CreateContent(); - - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); - - Assert.Equal(Status.Published, sut.Snapshot.Status); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) - ); - } - - [Fact] - public void ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time() - { - CreateContent(); - - var dueTime = Instant.MaxValue; - - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published, DueTime = dueTime })); - - Assert.Equal(Status.Draft, sut.Snapshot.Status); - Assert.Equal(Status.Published, sut.Snapshot.ScheduledTo); - Assert.Equal(dueTime, sut.Snapshot.ScheduledAt); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateContentCommand(new DeleteContent())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Delete(CreateContentCommand(new DeleteContent())); - }); - } - - [Fact] - public void Delete_should_update_properties_and_create_events() - { - CreateContent(); - - sut.Delete(CreateContentCommand(new DeleteContent())); - - Assert.True(sut.Snapshot.IsDeleted); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentDeleted()) - ); - } - - private void CreateContent() - { - sut.Create(CreateContentCommand(new CreateContent { Data = data })); - sut.ClearUncommittedEvents(); - } - - private void UpdateContent() - { - sut.Update(CreateContentCommand(new UpdateContent { Data = data })); - sut.ClearUncommittedEvents(); - } - - private void ChangeStatus(Status status) - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status })); - sut.ClearUncommittedEvents(); - } - - private void DeleteContent() - { - sut.Delete(CreateContentCommand(new DeleteContent())); - sut.ClearUncommittedEvents(); - } - - protected T CreateContentEvent(T @event) where T : ContentEvent - { - @event.ContentId = contentId; - - return CreateEvent(@event); - } - - protected T CreateContentCommand(T command) where T : ContentCommand - { - command.ContentId = contentId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs new file mode 100644 index 000000000..855fb6c44 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs @@ -0,0 +1,361 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Contents.State; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentGrainTests : HandlerTestBase + { + private readonly ISchemaEntity schema = A.Fake(); + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAppEntity app = A.Fake(); + private readonly ClaimsPrincipal user = new ClaimsPrincipal(); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); + + private readonly NamedContentData invalidData = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue(null)) + .AddField("my-field2", + new ContentFieldData() + .AddValue(1)); + private readonly NamedContentData data = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue("iv", 1)); + private readonly NamedContentData patch = + new NamedContentData() + .AddField("my-field2", + new ContentFieldData() + .AddValue("iv", 2)); + private readonly NamedContentData otherData = + new NamedContentData() + .AddField("my-field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("my-field2", + new ContentFieldData() + .AddValue("iv", 2)); + private readonly NamedContentData patched; + private readonly Guid contentId = Guid.NewGuid(); + private readonly ContentGrain sut; + + protected override Guid Id + { + get { return contentId; } + } + + public ContentGrainTests() + { + var schemaDef = + new Schema("my-schema") + .AddField(new NumberField(1, "my-field1", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true })) + .AddField(new NumberField(2, "my-field2", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = false })); + + A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); + + A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app); + A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)).Returns((app, schema)); + + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + A.CallTo(() => schema.ScriptCreate).Returns(""); + A.CallTo(() => schema.ScriptChange).Returns(""); + A.CallTo(() => schema.ScriptUpdate).Returns(""); + A.CallTo(() => schema.ScriptDelete).Returns(""); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.That.Matches(x => ReferenceEquals(x.Data, data)), A.Ignored)) + .Returns(data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.That.Matches(x => ReferenceEquals(x.Data, invalidData)), A.Ignored)) + .Returns(invalidData); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.That.Matches(x => ReferenceEquals(x.Data, patch)), A.Ignored)) + .Returns(patch); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.That.Matches(x => ReferenceEquals(x.Data, otherData)), A.Ignored)) + .Returns(otherData); + + patched = patch.MergeInto(data); + + sut = new ContentGrain(Store, appProvider, A.Dummy(), scriptEngine, A.Dummy()); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public async Task Command_should_throw_exception_if_content_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await Assert.ThrowsAsync(ExecuteUpdateAsync); + } + + [Fact] + public async Task Create_should_create_events_and_update_state() + { + var command = new CreateContent { Data = data }; + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(EntityCreatedResult.Create(data, 0)); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_also_publish() + { + var command = new CreateContent { Data = data, Publish = true }; + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(EntityCreatedResult.Create(data, 1)); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }), + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Create_should_throw_when_invalid_data_is_passed() + { + var command = new CreateContent { Data = invalidData }; + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); + } + + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = new UpdateContent { Data = otherData }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new ContentDataChangedResult(otherData, 1)); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_not_create_event_for_same_data() + { + var command = new UpdateContent { Data = data }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new ContentDataChangedResult(data, 0)); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_throw_when_invalid_data_is_passed() + { + var command = new UpdateContent { Data = invalidData }; + + await ExecuteCreateAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(CreateContentCommand(command))); + } + + [Fact] + public async Task Patch_should_create_events_and_update_state() + { + var command = new PatchContent { Data = patch }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new ContentDataChangedResult(otherData, 1)); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = patched }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustHaveHappened(); + } + + [Fact] + public async Task Patch_should_not_create_event_for_same_data() + { + var command = new PatchContent { Data = data }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new ContentDataChangedResult(data, 0)); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data }) + ); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangedStatus_should_create_events_and_update_state() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.Equal(Status.Published, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustHaveHappened(); + } + + [Fact] + public async Task ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time() + { + var dueTime = Instant.MaxValue; + + var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTime }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.Equal(Status.Draft, sut.Snapshot.Status); + Assert.Equal(Status.Published, sut.Snapshot.ScheduledTo); + Assert.Equal(dueTime, sut.Snapshot.ScheduledAt); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime }) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Delete_should_update_properties_and_create_events() + { + var command = new DeleteContent(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.True(sut.Snapshot.IsDeleted); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDeleted()) + ); + + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) + .MustHaveHappened(); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new CreateContent { Data = data })); + } + + private Task ExecuteUpdateAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = data })); + } + + private Task ExecuteDeleteAsync() + { + return sut.ExecuteAsync(CreateContentCommand(new DeleteContent())); + } + + protected T CreateContentEvent(T @event) where T : ContentEvent + { + @event.ContentId = contentId; + + return CreateEvent(@event); + } + + protected T CreateContentCommand(T command) where T : ContentCommand + { + command.ContentId = contentId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs deleted file mode 100644 index 863bd6d97..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules -{ - public class RuleCommandMiddlewareTests : HandlerTestBase - { - private readonly IAppProvider appProvider = A.Fake(); - 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; - - protected override Guid Id - { - get { return ruleId; } - } - - public RuleCommandMiddlewareTests() - { - A.CallTo(() => appProvider.GetSchemaAsync(A.Ignored, A.Ignored, false)) - .Returns(A.Fake()); - - sut = new RuleCommandMiddleware(Handler, appProvider); - } - - [Fact] - public async Task Create_should_create_domain_object() - { - var context = CreateContextForCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }); - - await TestCreate(rule, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - var context = CreateContextForCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }); - - CreateRule(); - - await TestUpdate(rule, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task Enable_should_update_domain_object() - { - CreateRule(); - DisableRule(); - - var command = CreateContextForCommand(new EnableRule { RuleId = ruleId }); - - await TestUpdate(rule, async _ => - { - await sut.HandleAsync(command); - }); - } - - [Fact] - public async Task Disable_should_update_domain_object() - { - CreateRule(); - - var command = CreateContextForCommand(new DisableRule { RuleId = ruleId }); - - await TestUpdate(rule, async _ => - { - await sut.HandleAsync(command); - }); - } - - [Fact] - public async Task Delete_should_update_domain_object() - { - CreateRule(); - - var command = CreateContextForCommand(new DeleteRule { RuleId = ruleId }); - - await TestUpdate(rule, async _ => - { - await sut.HandleAsync(command); - }); - } - - private void DisableRule() - { - rule.Disable(CreateCommand(new DisableRule())); - } - - private void CreateRule() - { - rule.Create(CreateCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs deleted file mode 100644 index 0afbaab17..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs +++ /dev/null @@ -1,256 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Rules; -using Squidex.Domain.Apps.Core.Rules.Actions; -using Squidex.Domain.Apps.Core.Rules.Triggers; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; -using Xunit; - -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 = new RuleDomainObject(); - - protected override Guid Id - { - get { return ruleId; } - } - - public RuleDomainObjectTests() - { - sut.ActivateAsync(Id, A.Fake>()); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); - - Assert.Throws(() => - { - sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); - }); - } - - [Fact] - public void Create_should_create_events() - { - var command = new CreateRule { Trigger = ruleTrigger, Action = ruleAction }; - - sut.Create(CreateRuleCommand(command)); - - Assert.Equal(AppId, sut.Snapshot.AppId.Id); - - Assert.Same(ruleTrigger, sut.Snapshot.RuleDef.Trigger); - Assert.Same(ruleAction, sut.Snapshot.RuleDef.Action); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleCreated { Trigger = ruleTrigger, Action = ruleAction }) - ); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_rule_is_deleted() - { - CreateRule(); - DeleteRule(); - - Assert.Throws(() => - { - sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction })); - }); - } - - [Fact] - public void Update_should_create_events() - { - var newTrigger = new ContentChangedTrigger - { - Schemas = ImmutableList.Empty - }; - - var newAction = new WebhookAction - { - Url = new Uri("https://squidex.io/v2") - }; - - CreateRule(); - - var command = new UpdateRule { Trigger = newTrigger, Action = newAction }; - - sut.Update(CreateRuleCommand(command)); - - Assert.Same(newTrigger, sut.Snapshot.RuleDef.Trigger); - Assert.Same(newAction, sut.Snapshot.RuleDef.Action); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleUpdated { Trigger = newTrigger, Action = newAction }) - ); - } - - [Fact] - public void Enable_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Enable(CreateRuleCommand(new EnableRule())); - }); - } - - [Fact] - public void Enable_should_throw_exception_if_rule_is_deleted() - { - CreateRule(); - DeleteRule(); - - Assert.Throws(() => - { - sut.Enable(CreateRuleCommand(new EnableRule())); - }); - } - - [Fact] - public void Enable_should_create_events() - { - CreateRule(); - - var command = new EnableRule(); - - sut.Enable(CreateRuleCommand(command)); - - Assert.True(sut.Snapshot.RuleDef.IsEnabled); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleEnabled()) - ); - } - - [Fact] - public void Disable_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Disable(CreateRuleCommand(new DisableRule())); - }); - } - - [Fact] - public void Disable_should_throw_exception_if_rule_is_deleted() - { - CreateRule(); - DeleteRule(); - - Assert.Throws(() => - { - sut.Disable(CreateRuleCommand(new DisableRule())); - }); - } - - [Fact] - public void Disable_should_create_events() - { - CreateRule(); - - var command = new DisableRule(); - - sut.Disable(CreateRuleCommand(command)); - - Assert.False(sut.Snapshot.RuleDef.IsEnabled); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleDisabled()) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateRuleCommand(new DeleteRule())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateRule(); - DeleteRule(); - - Assert.Throws(() => - { - sut.Delete(CreateRuleCommand(new DeleteRule())); - }); - } - - [Fact] - public void Delete_should_update_create_events() - { - CreateRule(); - - sut.Delete(CreateRuleCommand(new DeleteRule())); - - Assert.True(sut.Snapshot.IsDeleted); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateRuleEvent(new RuleDeleted()) - ); - } - - private void CreateRule() - { - sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction })); - sut.ClearUncommittedEvents(); - } - - private void DeleteRule() - { - sut.Delete(CreateRuleCommand(new DeleteRule())); - sut.ClearUncommittedEvents(); - } - - protected T CreateRuleEvent(T @event) where T : RuleEvent - { - @event.RuleId = ruleId; - - return CreateEvent(@event); - } - - protected T CreateRuleCommand(T command) where T : RuleCommand - { - command.RuleId = ruleId; - - return CreateCommand(command); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs new file mode 100644 index 000000000..bb5e414c8 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs @@ -0,0 +1,214 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Rules.Actions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Domain.Apps.Entities.Rules.Commands; +using Squidex.Domain.Apps.Entities.Rules.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public class RuleGrainTests : HandlerTestBase + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly Guid ruleId = Guid.NewGuid(); + private readonly RuleGrain sut; + + protected override Guid Id + { + get { return ruleId; } + } + + public RuleGrainTests() + { + sut = new RuleGrain(Store, appProvider); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public async Task Command_should_throw_exception_if_rule_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await Assert.ThrowsAsync(ExecuteDisableAsync); + } + + [Fact] + public async Task Create_should_create_events_and_update_state() + { + var command = MakeCreateCommand(); + + var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + + result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 0)); + + Assert.Equal(AppId, sut.Snapshot.AppId.Id); + + Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger); + Assert.Same(command.Action, sut.Snapshot.RuleDef.Action); + + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleCreated { Trigger = command.Trigger, Action = command.Action }) + ); + } + + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = MakeUpdateCommand(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger); + Assert.Same(command.Action, sut.Snapshot.RuleDef.Action); + + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleUpdated { Trigger = command.Trigger, Action = command.Action }) + ); + } + + [Fact] + public async Task Enable_should_handle_command() + { + await sut.ExecuteAsync(CreateRuleCommand(MakeCreateCommand())); + await sut.ExecuteAsync(CreateRuleCommand(new DisableRule())); + } + + [Fact] + public async Task Enable_should_create_events_and_update_state() + { + var command = new EnableRule(); + + await ExecuteCreateAsync(); + await ExecuteDisableAsync(); + + var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(2)); + + Assert.True(sut.Snapshot.RuleDef.IsEnabled); + + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleEnabled()) + ); + } + + [Fact] + public async Task Disable_should_create_events_and_update_state() + { + var command = new DisableRule(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.False(sut.Snapshot.RuleDef.IsEnabled); + + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDisabled()) + ); + } + + [Fact] + public async Task Delete_should_update_create_events() + { + var command = new DeleteRule(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.True(sut.Snapshot.IsDeleted); + + LastEvents + .ShouldHaveSameEvents( + CreateRuleEvent(new RuleDeleted()) + ); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateRuleCommand(MakeCreateCommand())); + } + + private Task ExecuteDisableAsync() + { + return sut.ExecuteAsync(CreateRuleCommand(new DisableRule())); + } + + private Task ExecuteDeleteAsync() + { + return sut.ExecuteAsync(CreateRuleCommand(new DeleteRule())); + } + + protected T CreateRuleEvent(T @event) where T : RuleEvent + { + @event.RuleId = ruleId; + + return CreateEvent(@event); + } + + protected T CreateRuleCommand(T command) where T : RuleCommand + { + command.RuleId = ruleId; + + return CreateCommand(command); + } + + private CreateRule MakeCreateCommand() + { + var newTrigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Empty + }; + + var newAction = new WebhookAction + { + Url = new Uri("https://squidex.io/v2") + }; + + return new CreateRule { Trigger = newTrigger, Action = newAction }; + } + + private static UpdateRule MakeUpdateCommand() + { + var newTrigger = new ContentChangedTrigger + { + Schemas = ImmutableList.Empty + }; + + var newAction = new WebhookAction + { + Url = new Uri("https://squidex.io/v2") + }; + + return new UpdateRule { Trigger = newTrigger, Action = newAction }; + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs deleted file mode 100644 index f6e5c4c43..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs +++ /dev/null @@ -1,280 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public class SchemaCommandMiddlewareTests : HandlerTestBase - { - private readonly IAppProvider appProvider = A.Fake(); - private readonly SchemaCommandMiddleware sut; - private readonly SchemaDomainObject schema; - private readonly FieldRegistry registry = new FieldRegistry(new TypeNameRegistry()); - private readonly string fieldName = "age"; - - protected override Guid Id - { - get { return SchemaId; } - } - - public SchemaCommandMiddlewareTests() - { - schema = new SchemaDomainObject(registry); - - sut = new SchemaCommandMiddleware(Handler, appProvider); - - A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName)) - .Returns((ISchemaEntity)null); - } - - [Fact] - public async Task Create_should_create_schema_domain_object() - { - var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - - await TestCreate(schema, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(SchemaId, context.Result>().IdOrValue); - - A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName)).MustHaveHappened(); - } - - [Fact] - public async Task UpdateSchema_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new UpdateSchema { Properties = new SchemaProperties() }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task ReorderSchema_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new ReorderFields { FieldIds = new List() }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task PublishSchema_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new PublishSchema()); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task UnpublishSchema_should_update_domain_object() - { - CreateSchema(); - PublishSchema(); - - var context = CreateContextForCommand(new UnpublishSchema()); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task ConfigureScripts_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new ConfigureScripts()); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task DeleteSchema_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new DeleteSchema()); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task Add_should_update_domain_object() - { - CreateSchema(); - - var context = CreateContextForCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(1, context.Result>().IdOrValue); - } - - [Fact] - public async Task UpdateField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task LockField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new LockField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task HideField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new HideField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task ShowField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - HideField(); - - var context = CreateContextForCommand(new ShowField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task DisableField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new DisableField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task EnableField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - DisableField(); - - var context = CreateContextForCommand(new EnableField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task DeleteField_should_update_domain_object() - { - CreateSchema(); - CreateField(); - - var context = CreateContextForCommand(new DeleteField { FieldId = 1 }); - - await TestUpdate(schema, async _ => - { - await sut.HandleAsync(context); - }); - } - - private void CreateSchema() - { - schema.Create(CreateCommand(new CreateSchema { Name = SchemaName })); - } - - private void PublishSchema() - { - schema.Publish(CreateCommand(new PublishSchema())); - } - - private void CreateField() - { - schema.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); - } - - private void HideField() - { - schema.HideField(CreateCommand(new HideField { FieldId = 1 })); - } - - private void DisableField() - { - schema.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs deleted file mode 100644 index 71922b093..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs +++ /dev/null @@ -1,665 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Schemas -{ - public class SchemaDomainObjectTests : HandlerTestBase - { - private readonly string fieldName = "age"; - private readonly NamedId fieldId; - private readonly SchemaDomainObject sut; - - protected override Guid Id - { - get { return SchemaId; } - } - - public SchemaDomainObjectTests() - { - fieldId = new NamedId(1, fieldName); - - var fieldRegistry = new FieldRegistry(new TypeNameRegistry()); - - sut = new SchemaDomainObject(fieldRegistry); - sut.ActivateAsync(Id, A.Fake>()); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName })); - - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName })); - }); - } - - [Fact] - public void Create_should_create_schema_and_create_events() - { - var properties = new SchemaProperties(); - - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties })); - - Assert.Equal(AppId, sut.Snapshot.AppId.Id); - - Assert.Equal(SchemaName, sut.Snapshot.Name); - Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties }) - ); - } - - [Fact] - public void Create_should_create_schema_with_initial_fields() - { - var properties = new SchemaProperties(); - - var fields = new List - { - new CreateSchemaField { Name = "field1", Properties = ValidProperties() }, - new CreateSchemaField { Name = "field2", Properties = ValidProperties() } - }; - - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, Properties = properties, Fields = fields })); - - var @event = (SchemaCreated)sut.GetUncomittedEvents().Single().Payload; - - Assert.Equal(AppId, sut.Snapshot.AppId.Id); - Assert.Equal(SchemaName, sut.Snapshot.Name); - Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); - - Assert.Equal(2, @event.Fields.Count); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() })); - }); - } - - [Fact] - public void Update_should_refresh_properties_and_create_events() - { - var properties = new SchemaProperties(); - - CreateSchema(); - - sut.Update(CreateCommand(new UpdateSchema { Properties = properties })); - - Assert.Equal(properties, sut.Snapshot.SchemaDef.Properties); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaUpdated { Properties = properties }) - ); - } - - [Fact] - public void ConfigureScripts_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ConfigureScripts(CreateCommand(new ConfigureScripts())); - }); - } - - [Fact] - public void ConfigureScripts_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.ConfigureScripts(CreateCommand(new ConfigureScripts())); - }); - } - - [Fact] - public void ConfigureScripts_should_create_events() - { - CreateSchema(); - - sut.ConfigureScripts(CreateCommand(new ConfigureScripts - { - ScriptQuery = "", - ScriptCreate = "", - ScriptUpdate = "", - ScriptDelete = "", - ScriptChange = "" - })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new ScriptsConfigured - { - ScriptQuery = "", - ScriptCreate = "", - ScriptUpdate = "", - ScriptDelete = "", - ScriptChange = "" - }) - ); - } - - [Fact] - public void Reorder_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List() })); - }); - } - - [Fact] - public void Reorder_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List() })); - }); - } - - [Fact] - public void Reorder_should_refresh_properties_and_create_events() - { - var fieldIds = new List { 1, 2 }; - - CreateSchema(); - - sut.Add(CreateCommand(new AddField { Name = "field1", Properties = ValidProperties() })); - sut.Add(CreateCommand(new AddField { Name = "field2", Properties = ValidProperties() })); - - sut.ClearUncommittedEvents(); - - sut.Reorder(CreateCommand(new ReorderFields { FieldIds = fieldIds })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaFieldsReordered { FieldIds = fieldIds }) - ); - } - - [Fact] - public void Publish_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Publish(CreateCommand(new PublishSchema())); - }); - } - - [Fact] - public void Publish_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Publish(CreateCommand(new PublishSchema())); - }); - } - - [Fact] - public void Publish_should_refresh_properties_and_create_events() - { - CreateSchema(); - - sut.Publish(CreateCommand(new PublishSchema())); - - Assert.True(sut.Snapshot.SchemaDef.IsPublished); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaPublished()) - ); - } - - [Fact] - public void Unpublish_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Unpublish(CreateCommand(new UnpublishSchema())); - }); - } - - [Fact] - public void Unpublish_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Unpublish(CreateCommand(new UnpublishSchema())); - }); - } - - [Fact] - public void Unpublish_should_refresh_properties_and_create_events() - { - CreateSchema(); - PublishSchema(); - - sut.Unpublish(CreateCommand(new UnpublishSchema())); - - Assert.False(sut.Snapshot.SchemaDef.IsPublished); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaUnpublished()) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateCommand(new DeleteSchema())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Delete(CreateCommand(new DeleteSchema())); - }); - } - - [Fact] - public void Delete_should_refresh_properties_and_create_events() - { - CreateSchema(); - - sut.Delete(CreateCommand(new DeleteSchema())); - - Assert.True(sut.Snapshot.IsDeleted); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new SchemaDeleted()) - ); - } - - [Fact] - public void AddField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = ValidProperties() })); - }); - } - - [Fact] - public void AddField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); - }); - } - - [Fact] - public void Add_should_update_schema_and_create_events() - { - var properties = new NumberFieldProperties(); - - CreateSchema(); - - sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = properties })); - - Assert.Equal(properties, sut.Snapshot.SchemaDef.FieldsById[1].RawProperties); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldAdded { Name = fieldName, FieldId = fieldId, Properties = properties }) - ); - } - - [Fact] - public void UpdateField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); - }); - } - - [Fact] - public void UpdateField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); - }); - } - - [Fact] - public void UpdateField_should_update_schema_and_create_events() - { - var properties = new NumberFieldProperties(); - - CreateSchema(); - CreateField(); - - sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = properties })); - - Assert.Equal(properties, sut.Snapshot.SchemaDef.FieldsById[1].RawProperties); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldUpdated { FieldId = fieldId, Properties = properties }) - ); - } - - [Fact] - public void LockField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.LockField(CreateCommand(new LockField { FieldId = 1 })); - }); - } - - [Fact] - public void LockField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.LockField(CreateCommand(new LockField { FieldId = 1 })); - }); - } - - [Fact] - public void LockField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.LockField(CreateCommand(new LockField { FieldId = 1 })); - - Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldLocked { FieldId = fieldId }) - ); - } - - [Fact] - public void HideField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - }); - } - - [Fact] - public void HideField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - }); - } - - [Fact] - public void HideField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - - Assert.True(sut.Snapshot.SchemaDef.FieldsById[1].IsHidden); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldHidden { FieldId = fieldId }) - ); - } - - [Fact] - public void ShowField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); - }); - } - - [Fact] - public void ShowField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); - }); - } - - [Fact] - public void ShowField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); - - Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsHidden); - - sut.GetUncomittedEvents().Skip(1) - .ShouldHaveSameEvents( - CreateEvent(new FieldShown { FieldId = fieldId }) - ); - } - - [Fact] - public void DisableField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - }); - } - - [Fact] - public void DisableField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - }); - } - - [Fact] - public void DisableField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - - Assert.True(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldDisabled { FieldId = fieldId }) - ); - } - - [Fact] - public void EnableField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); - }); - } - - [Fact] - public void EnableField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); - }); - } - - [Fact] - public void EnableField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); - - Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled); - - sut.GetUncomittedEvents().Skip(1) - .ShouldHaveSameEvents( - CreateEvent(new FieldEnabled { FieldId = fieldId }) - ); - } - - [Fact] - public void DeleteField_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); - }); - } - - [Fact] - public void DeleteField_should_throw_exception_if_schema_is_deleted() - { - CreateSchema(); - DeleteSchema(); - - Assert.Throws(() => - { - sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); - }); - } - - [Fact] - public void DeleteField_should_update_schema_and_create_events() - { - CreateSchema(); - CreateField(); - - sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); - - Assert.False(sut.Snapshot.SchemaDef.FieldsById.ContainsKey(1)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new FieldDeleted { FieldId = fieldId }) - ); - } - - private void CreateField() - { - sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); - sut.ClearUncommittedEvents(); - } - - private void CreateSchema() - { - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName })); - sut.ClearUncommittedEvents(); - } - - private void PublishSchema() - { - sut.Publish(CreateCommand(new PublishSchema())); - sut.ClearUncommittedEvents(); - } - - private void DeleteSchema() - { - sut.Delete(CreateCommand(new DeleteSchema())); - sut.ClearUncommittedEvents(); - } - - private static StringFieldProperties ValidProperties() - { - return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; - } - - private static StringFieldProperties InvalidProperties() - { - return new StringFieldProperties { MinLength = 20, MaxLength = 10 }; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs new file mode 100644 index 000000000..e1fd8c7bd --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs @@ -0,0 +1,428 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Domain.Apps.Events.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public class SchemaGrainTests : HandlerTestBase + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly FieldRegistry registry = new FieldRegistry(new TypeNameRegistry()); + private readonly string fieldName = "age"; + private readonly NamedId fieldId; + private readonly SchemaGrain sut; + + protected override Guid Id + { + get { return SchemaId; } + } + + public SchemaGrainTests() + { + A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName)) + .Returns((ISchemaEntity)null); + + fieldId = new NamedId(1, fieldName); + + sut = new SchemaGrain(Store, appProvider, registry); + sut.ActivateAsync(Id).Wait(); + } + + [Fact] + public async Task Command_should_throw_exception_if_rule_is_deleted() + { + await ExecuteCreateAsync(); + await ExecuteDeleteAsync(); + + await Assert.ThrowsAsync(ExecutePublishAsync); + } + + [Fact] + public async Task Create_should_create_schema_and_create_events() + { + var properties = new SchemaProperties(); + + var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties }; + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 0)); + + Assert.Equal(AppId, sut.Snapshot.AppId.Id); + + Assert.Equal(SchemaName, sut.Snapshot.Name); + Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties }) + ); + } + + [Fact] + public async Task Create_should_create_schema_with_initial_fields() + { + var properties = new SchemaProperties(); + + var fields = new List + { + new CreateSchemaField { Name = "field1", Properties = ValidProperties() }, + new CreateSchemaField { Name = "field2", Properties = ValidProperties() } + }; + + var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Fields = fields }; + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 0)); + + var @event = (SchemaCreated)LastEvents.Single().Payload; + + Assert.Equal(AppId, sut.Snapshot.AppId.Id); + Assert.Equal(SchemaName, sut.Snapshot.Name); + Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name); + + Assert.Equal(2, @event.Fields.Count); + } + + [Fact] + public async Task Update_should_create_events_and_update_state() + { + var command = new UpdateSchema { Properties = new SchemaProperties() }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaUpdated { Properties = command.Properties }) + ); + } + + [Fact] + public async Task ConfigureScripts_should_create_events() + { + var command = new ConfigureScripts + { + ScriptQuery = "", + ScriptCreate = "", + ScriptUpdate = "", + ScriptDelete = "", + ScriptChange = "" + }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new ScriptsConfigured + { + ScriptQuery = "", + ScriptCreate = "", + ScriptUpdate = "", + ScriptDelete = "", + ScriptChange = "" + }) + ); + } + + [Fact] + public async Task Reorder_should_create_events_and_update_state() + { + var command = new ReorderFields { FieldIds = new List { 1, 2 } }; + + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync("field1"); + await ExecuteAddFieldAsync("field2"); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(3)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaFieldsReordered { FieldIds = command.FieldIds }) + ); + } + + [Fact] + public async Task Publish_should_create_events_and_update_state() + { + var command = new PublishSchema(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.True(sut.Snapshot.SchemaDef.IsPublished); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaPublished()) + ); + } + + [Fact] + public async Task Unpublish_should_create_events_and_update_state() + { + var command = new UnpublishSchema(); + + await ExecuteCreateAsync(); + await ExecutePublishAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(2)); + + Assert.False(sut.Snapshot.SchemaDef.IsPublished); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaUnpublished()) + ); + } + + [Fact] + public async Task Delete_should_create_events_and_update_state() + { + var command = new DeleteSchema(); + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(1)); + + Assert.True(sut.Snapshot.IsDeleted); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaDeleted()) + ); + } + + [Fact] + public async Task Add_should_create_events_and_update_state() + { + var command = new AddField { Name = fieldName, Properties = ValidProperties() }; + + await ExecuteCreateAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(EntityCreatedResult.Create(1, 1)); + + Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.FieldsById[1].RawProperties); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldAdded { Name = fieldName, FieldId = fieldId, Properties = command.Properties }) + ); + } + + [Fact] + public async Task UpdateField_should_create_events_and_update_state() + { + var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; + + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(2)); + + Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.FieldsById[1].RawProperties); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldUpdated { FieldId = fieldId, Properties = command.Properties }) + ); + } + + [Fact] + public async Task LockField_should_create_events_and_update_state() + { + var command = new LockField { FieldId = 1 }; + + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(2)); + + Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldLocked { FieldId = fieldId }) + ); + } + + [Fact] + public async Task HideField_should_create_events_and_update_state() + { + var command = new HideField { FieldId = 1 }; + + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(2)); + + Assert.True(sut.Snapshot.SchemaDef.FieldsById[1].IsHidden); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldHidden { FieldId = fieldId }) + ); + } + + [Fact] + public async Task ShowField_should_create_events_and_update_state() + { + var command = new ShowField { FieldId = 1 }; + + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + await ExecuteHideFieldAsync(1); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(3)); + + Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsHidden); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldShown { FieldId = fieldId }) + ); + } + + [Fact] + public async Task DisableField_should_create_events_and_update_state() + { + var command = new DisableField { FieldId = 1 }; + + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(2)); + + Assert.True(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldDisabled { FieldId = fieldId }) + ); + } + + [Fact] + public async Task EnableField_should_create_events_and_update_state() + { + var command = new EnableField { FieldId = 1 }; + + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + await ExecuteDisableFieldAsync(1); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(3)); + + Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldEnabled { FieldId = fieldId }) + ); + } + + [Fact] + public async Task DeleteField_should_create_events_and_update_state() + { + var command = new DeleteField { FieldId = 1 }; + + await ExecuteCreateAsync(); + await ExecuteAddFieldAsync(fieldName); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(new EntitySavedResult(2)); + + Assert.False(sut.Snapshot.SchemaDef.FieldsById.ContainsKey(1)); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new FieldDeleted { FieldId = fieldId }) + ); + } + + private Task ExecuteCreateAsync() + { + return sut.ExecuteAsync(CreateCommand(new CreateSchema { Name = SchemaName })); + } + + private Task ExecuteAddFieldAsync(string name) + { + return sut.ExecuteAsync(CreateCommand(new AddField { Properties = ValidProperties(), Name = name })); + } + + private Task ExecuteHideFieldAsync(long id) + { + return sut.ExecuteAsync(CreateCommand(new HideField { FieldId = id })); + } + + private Task ExecuteDisableFieldAsync(long id) + { + return sut.ExecuteAsync(CreateCommand(new DisableField { FieldId = id })); + } + + private Task ExecutePublishAsync() + { + return sut.ExecuteAsync(CreateCommand(new PublishSchema())); + } + + private Task ExecuteDeleteAsync() + { + return sut.ExecuteAsync(CreateCommand(new DeleteSchema())); + } + + private static StringFieldProperties ValidProperties() + { + return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs index f3251657d..b89bc6962 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs @@ -40,5 +40,10 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { lhs.Should().BeOfType(rhs.GetType()); } + + public static void ShouldBeEquivalent(this T result, T value) + { + result.ShouldBeEquivalentTo(value, o => o.IncludingAllDeclaredProperties()); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index 0822c2f2c..e2021304d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -6,68 +6,24 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; #pragma warning disable IDE0019 // Use pattern matching namespace Squidex.Domain.Apps.Entities.TestHelpers { - public abstract class HandlerTestBase where T : IDomainObject + public abstract class HandlerTestBase where T : IDomainObjectGrain { - private sealed class MockupHandler : IAggregateHandler - { - private T domainObject; - - public bool IsCreated { get; private set; } - public bool IsUpdated { get; private set; } - - public void Init(T newDomainObject) - { - domainObject = newDomainObject; - - IsCreated = false; - IsUpdated = false; - } - - public Task CreateSyncedAsync(CommandContext context, Func creator) where V : class, IDomainObject - { - return CreateAsync(context, creator); - } - - public Task UpdateSyncedAsync(CommandContext context, Func creator) where V : class, IDomainObject - { - return UpdateAsync(context, creator); - } - - public async Task CreateAsync(CommandContext context, Func creator) where V : class, IDomainObject - { - IsCreated = true; - - var @do = domainObject as V; - - await creator(domainObject as V); - - return @do; - } - - public async Task UpdateAsync(CommandContext context, Func updater) where V : class, IDomainObject - { - IsUpdated = true; - - var @do = domainObject as V; - - await updater(domainObject as V); - - return @do; - } - } - - private readonly MockupHandler handler = new MockupHandler(); + private readonly IStore store = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); protected RefToken User { get; } = new RefToken("subject", Guid.NewGuid().ToString()); @@ -75,8 +31,6 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers protected Guid SchemaId { get; } = Guid.NewGuid(); - protected abstract Guid Id { get; } - protected string AppName { get; } = "my-app"; protected string SchemaName { get; } = "my-schema"; @@ -91,44 +45,36 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers get { return new NamedId(SchemaId, SchemaName); } } - protected IAggregateHandler Handler - { - get { return handler; } - } + protected abstract Guid Id { get; } - protected CommandContext CreateContextForCommand(TCommand command) where TCommand : SquidexCommand + public IStore Store { - return new CommandContext(CreateCommand(command), A.Dummy()); + get { return store; } } - protected async Task TestCreate(T domainObject, Func action, bool shouldCreate = true) - { - handler.Init(domainObject); - - await domainObject.ActivateAsync(Id, A.Fake>()); - await action(domainObject); + public IEnumerable> LastEvents { get; private set; } = Enumerable.Empty>(); - if (!handler.IsCreated && shouldCreate) - { - throw new InvalidOperationException("Create not called."); - } + protected HandlerTestBase() + { + A.CallTo(() => store.WithSnapshotsAndEventSourcing(A.Ignored, Id, A>.Ignored, A, Task>>.Ignored)) + .Returns(persistence); + + A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) + .Invokes(new Action>>(events => + { + LastEvents = events; + })); } - protected async Task TestUpdate(T domainObject, Func action, bool shouldUpdate = true) + protected CommandContext CreateContextForCommand(TCommand command) where TCommand : SquidexCommand { - handler.Init(domainObject); - - await domainObject.ActivateAsync(Id, A.Fake>()); - await action(domainObject); - - if (!handler.IsUpdated && shouldUpdate) - { - throw new InvalidOperationException("Update not called."); - } + return new CommandContext(CreateCommand(command), A.Dummy()); } protected TCommand CreateCommand(TCommand command) where TCommand : SquidexCommand { + command.ExpectedVersion = EtagVersion.Any; + if (command.Actor == null) { command.Actor = User; @@ -151,21 +97,26 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { @event.Actor = User; - var appEvent = @event as AppEvent; + EnrichAppInfo(@event); + EnrichSchemaInfo(@event); + + return @event; + } - if (appEvent != null) + private void EnrichAppInfo(IEvent @event) + { + if (@event is AppEvent appEvent) { appEvent.AppId = AppNamedId; } + } - var schemaEvent = @event as SchemaEvent; - - if (schemaEvent != null) + private void EnrichSchemaInfo(IEvent @event) + { + if (@event is SchemaEvent schemaEvent) { schemaEvent.SchemaId = SchemaNamedId; } - - return @event; } } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs deleted file mode 100644 index f1c9113d0..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs +++ /dev/null @@ -1,284 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class AggregateHandlerTests - { - private readonly IServiceProvider serviceProvider = A.Fake(); - private readonly IStore store = A.Fake>(); - private readonly IStateFactory stateFactory = A.Fake(); - private readonly IPersistence persistence = A.Fake>(); - private readonly Envelope event1 = new Envelope(new MyEvent()); - private readonly Envelope event2 = new Envelope(new MyEvent()); - private readonly CommandContext context; - private readonly CommandContext invalidContext = new CommandContext(A.Dummy(), A.Dummy()); - private readonly Guid domainObjectId = Guid.NewGuid(); - private readonly MyCommand command; - private readonly MyDomainObject domainObject = new MyDomainObject(); - private readonly AggregateHandler sut; - - public AggregateHandlerTests() - { - command = new MyCommand { AggregateId = domainObjectId, ExpectedVersion = EtagVersion.Any }; - context = new CommandContext(command, A.Dummy()); - - A.CallTo(() => store.WithSnapshotsAndEventSourcing(domainObjectId, A>.Ignored, A, Task>>.Ignored)) - .Returns(persistence); - - A.CallTo(() => stateFactory.CreateAsync(domainObjectId)) - .Returns(Task.FromResult(domainObject)); - - A.CallTo(() => stateFactory.GetSingleAsync(domainObjectId)) - .Returns(Task.FromResult(domainObject)); - - sut = new AggregateHandler(stateFactory, serviceProvider); - - domainObject.ActivateAsync(domainObjectId, store).Wait(); - } - - [Fact] - public Task Create_with_task_should_throw_exception_if_not_aggregate_command() - { - return Assert.ThrowsAnyAsync(() => sut.CreateAsync(invalidContext, x => TaskHelper.False)); - } - - [Fact] - public Task Create_synced_with_task_should_throw_exception_if_not_aggregate_command() - { - return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(invalidContext, x => TaskHelper.False)); - } - - [Fact] - public Task Create_with_task_should_should_throw_exception_if_version_is_wrong() - { - command.ExpectedVersion = 2; - - return Assert.ThrowsAnyAsync(() => sut.CreateAsync(context, x => TaskHelper.False)); - } - - [Fact] - public Task Create_synced_with_task_should_should_throw_exception_if_version_is_wrong() - { - command.ExpectedVersion = 2; - - return Assert.ThrowsAnyAsync(() => sut.CreateSyncedAsync(context, x => TaskHelper.False)); - } - - [Fact] - public async Task Create_with_task_should_create_domain_object_and_save() - { - MyDomainObject passedDomainObject = null; - - await sut.CreateAsync(context, async x => - { - x.RaiseEvent(new MyEvent()); - - await Task.Yield(); - - passedDomainObject = x; - }); - - Assert.Equal(domainObject, passedDomainObject); - Assert.NotNull(context.Result>()); - - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Create_synced_with_task_should_create_domain_object_and_save() - { - MyDomainObject passedDomainObject = null; - - await sut.CreateSyncedAsync(context, async x => - { - x.RaiseEvent(new MyEvent()); - x.RaiseEvent(new MyEvent()); - - await Task.Yield(); - - passedDomainObject = x; - }); - - Assert.Equal(2, domainObject.Snapshot.Version); - Assert.Equal(domainObject, passedDomainObject); - Assert.NotNull(context.Result>()); - - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Create_should_create_domain_object_and_save() - { - MyDomainObject passedDomainObject = null; - - await sut.CreateAsync(context, x => - { - x.RaiseEvent(new MyEvent()); - x.RaiseEvent(new MyEvent()); - - passedDomainObject = x; - }); - - Assert.Equal(2, domainObject.Snapshot.Version); - Assert.Equal(domainObject, passedDomainObject); - Assert.NotNull(context.Result>()); - - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Create_synced_should_create_domain_object_and_save() - { - MyDomainObject passedDomainObject = null; - - await sut.CreateSyncedAsync(context, x => - { - x.RaiseEvent(new MyEvent()); - x.RaiseEvent(new MyEvent()); - - passedDomainObject = x; - }); - - Assert.Equal(2, domainObject.Snapshot.Version); - Assert.Equal(domainObject, passedDomainObject); - Assert.NotNull(context.Result>()); - - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public Task Update_with_task_should_throw_exception_if_not_aggregate_command() - { - return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(invalidContext, x => TaskHelper.False)); - } - - [Fact] - public Task Update_synced_with_task_should_throw_exception_if_not_aggregate_command() - { - return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(invalidContext, x => TaskHelper.False)); - } - - [Fact] - public Task Update_with_task_should_should_throw_exception_if_version_is_wrong() - { - command.ExpectedVersion = 2; - - return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(context, x => TaskHelper.False)); - } - - [Fact] - public Task Update_synced_with_task_should_should_throw_exception_if_version_is_wrong() - { - command.ExpectedVersion = 2; - - return Assert.ThrowsAnyAsync(() => sut.UpdateSyncedAsync(context, x => TaskHelper.False)); - } - - [Fact] - public async Task Update_with_task_should_create_domain_object_and_save() - { - MyDomainObject passedDomainObject = null; - - await sut.UpdateAsync(context, async x => - { - x.RaiseEvent(new MyEvent()); - x.RaiseEvent(new MyEvent()); - - await Task.Yield(); - - passedDomainObject = x; - }); - - Assert.Equal(2, domainObject.Snapshot.Version); - Assert.Equal(domainObject, passedDomainObject); - Assert.NotNull(context.Result()); - - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Update_synced_with_task_should_create_domain_object_and_save() - { - MyDomainObject passedDomainObject = null; - - await sut.UpdateSyncedAsync(context, async x => - { - x.RaiseEvent(new MyEvent()); - x.RaiseEvent(new MyEvent()); - - await Task.Yield(); - - passedDomainObject = x; - }); - - Assert.Equal(2, domainObject.Snapshot.Version); - Assert.Equal(domainObject, passedDomainObject); - Assert.NotNull(context.Result()); - - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Update_should_create_domain_object_and_save() - { - MyDomainObject passedDomainObject = null; - - await sut.UpdateAsync(context, x => - { - x.RaiseEvent(new MyEvent()); - x.RaiseEvent(new MyEvent()); - - passedDomainObject = x; - }); - - Assert.Equal(2, domainObject.Snapshot.Version); - Assert.Equal(domainObject, passedDomainObject); - Assert.NotNull(context.Result()); - - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .MustHaveHappened(); - } - - [Fact] - public async Task Update_synced_should_create_domain_object_and_save() - { - MyDomainObject passedDomainObject = null; - - await sut.UpdateSyncedAsync(context, x => - { - x.RaiseEvent(new MyEvent()); - x.RaiseEvent(new MyEvent()); - - passedDomainObject = x; - }); - - Assert.Equal(2, domainObject.Snapshot.Version); - Assert.Equal(domainObject, passedDomainObject); - Assert.NotNull(context.Result()); - - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs deleted file mode 100644 index 70e23f0b8..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class DomainObjectBaseTests - { - private readonly IStore store = A.Fake>(); - private readonly IPersistence persistence = A.Fake>(); - private readonly Guid id = Guid.NewGuid(); - private readonly MyDomainObject sut = new MyDomainObject(); - - public DomainObjectBaseTests() - { - A.CallTo(() => store.WithSnapshotsAndEventSourcing(id, A>.Ignored, A, Task>>.Ignored)) - .Returns(persistence); - } - - [Fact] - public void Should_instantiate() - { - Assert.Equal(EtagVersion.Empty, sut.Version); - } - - [Fact] - public async Task Should_write_state_and_events_when_saved() - { - await sut.ActivateAsync(id, store); - - var event1 = new MyEvent(); - var event2 = new MyEvent(); - var newState = new MyDomainState(); - - sut.RaiseEvent(event1); - sut.RaiseEvent(event2); - sut.ApplySnapshot(newState); - - await sut.WriteAsync(); - - A.CallTo(() => persistence.WriteSnapshotAsync(newState)) - .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 2))) - .MustHaveHappened(); - - Assert.Empty(sut.GetUncomittedEvents()); - } - - [Fact] - public async Task Should_not_ignore_exception_when_saving() - { - A.CallTo(() => persistence.WriteEventsAsync(A>>.Ignored)) - .Throws(new InvalidOperationException()); - - await sut.ActivateAsync(id, store); - - var event1 = new MyEvent(); - var event2 = new MyEvent(); - var newState = new MyDomainState(); - - sut.RaiseEvent(event1); - sut.RaiseEvent(event2); - sut.ApplySnapshot(newState); - - await Assert.ThrowsAsync(() => sut.WriteAsync()); - - A.CallTo(() => persistence.WriteSnapshotAsync(newState)) - .MustNotHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 2))) - .MustHaveHappened(); - - Assert.Empty(sut.GetUncomittedEvents()); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs new file mode 100644 index 000000000..169e59ff8 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -0,0 +1,245 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class DomainObjectGrainTests + { + private readonly IStore store = A.Fake>(); + private readonly IPersistence persistence = A.Fake>(); + private readonly Guid id = Guid.NewGuid(); + private readonly MyGrain sut; + + public sealed class MyDomainState : IDomainState + { + public long Version { get; set; } + + public int Value { get; set; } + } + + public sealed class ValueChanged : IEvent + { + public int Value { get; set; } + } + + public sealed class CreateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class CreateCustom : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateAuto : MyCommand + { + public int Value { get; set; } + } + + public sealed class UpdateCustom : MyCommand + { + public int Value { get; set; } + } + + public sealed class MyGrain : DomainObjectGrain + { + public MyGrain(IStore store) + : base(store) + { + } + + public override Task ExecuteAsync(IAggregateCommand command) + { + switch (command) + { + case CreateAuto createAuto: + return CreateAsync(createAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case CreateCustom createCustom: + return CreateReturnAsync(createCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "CREATED"; + }); + + case UpdateAuto updateAuto: + return UpdateAsync(updateAuto, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + }); + + case UpdateCustom updateCustom: + return UpdateReturnAsync(updateCustom, c => + { + RaiseEvent(new ValueChanged { Value = c.Value }); + + return "UPDATED"; + }); + } + + return Task.FromResult(null); + } + + public override void ApplyEvent(Envelope @event) + { + if (@event.Payload is ValueChanged valueChanged) + { + ApplySnapshot(new MyDomainState { Value = valueChanged.Value }); + } + } + } + + public DomainObjectGrainTests() + { + A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyGrain), id, A>.Ignored, A, Task>>.Ignored)) + .Returns(persistence); + + sut = new MyGrain(store); + } + + [Fact] + public void Should_instantiate() + { + Assert.Equal(EtagVersion.Empty, sut.Version); + } + + [Fact] + public async Task Should_write_state_and_events_when_created() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(new CreateAuto { Value = 5 }); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 5))) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result is EntityCreatedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + Assert.Equal(5, sut.Snapshot.Value); + } + + [Fact] + public async Task Should_write_state_and_events_when_updated() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(new UpdateAuto { Value = 5 }); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 5))) + .MustHaveHappened(); + A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) + .MustHaveHappened(); + + Assert.True(result is EntitySavedResult); + + Assert.Empty(sut.GetUncomittedEvents()); + Assert.Equal(5, sut.Snapshot.Value); + } + + [Fact] + public async Task Should_throw_exception_when_already_created() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); + } + + [Fact] + public async Task Should_throw_exception_when_not_created() + { + await SetupEmptyAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); + } + + [Fact] + public async Task Should_return_custom_result_on_create() + { + await SetupEmptyAsync(); + + var result = await sut.ExecuteAsync(new CreateCustom()); + + Assert.Equal("CREATED", result); + } + + [Fact] + public async Task Should_return_custom_result_on_update() + { + await SetupCreatedAsync(); + + var result = await sut.ExecuteAsync(new UpdateCustom()); + + Assert.Equal("UPDATED", result); + } + + [Fact] + public async Task Should_throw_exception_when_other_verison_expected() + { + await SetupCreatedAsync(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 })); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_create_failed() + { + await SetupEmptyAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); + + Assert.Empty(sut.GetUncomittedEvents()); + Assert.Equal(0, sut.Snapshot.Value); + } + + [Fact] + public async Task Should_reset_state_when_writing_snapshot_for_update_failed() + { + await SetupCreatedAsync(); + + A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); + + Assert.Empty(sut.GetUncomittedEvents()); + Assert.Equal(0, sut.Snapshot.Value); + } + + private async Task SetupCreatedAsync() + { + await sut.ActivateAsync(id); + + await sut.ExecuteAsync(new CreateAuto()); + } + + private async Task SetupEmptyAsync() + { + await sut.ActivateAsync(id); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/GrainCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/GrainCommandMiddlewareTests.cs new file mode 100644 index 000000000..ca97ee36f --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Commands/GrainCommandMiddlewareTests.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class GrainCommandMiddlewareTests + { + private readonly IStateFactory factory = A.Fake(); + private readonly MyGrain grain = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly GrainCommandMiddleware sut; + + public class MatchingCommand : MyCommand + { + } + + public GrainCommandMiddlewareTests() + { + A.CallTo(() => factory.CreateAsync(id)) + .Returns(grain); + + sut = new GrainCommandMiddleware(factory); + } + + [Fact] + public async Task Should_invoke_grain_when_command_is_correct() + { + var command = new MatchingCommand { AggregateId = id }; + var context = new CommandContext(command, A.Fake()); + + A.CallTo(() => grain.ExecuteAsync(command)) + .Returns(100); + + await sut.HandleAsync(context); + + Assert.Equal(100, context.Result()); + } + + [Fact] + public async Task Should_not_invoke_grain_when_command_is_not_correct() + { + var command = new MyCommand { AggregateId = id }; + var context = new CommandContext(command, A.Fake()); + + await sut.HandleAsync(context); + + Assert.Null(context.Result()); + + A.CallTo(() => grain.ExecuteAsync(command)) + .MustNotHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/SyncedGrainCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/SyncedGrainCommandMiddlewareTests.cs new file mode 100644 index 000000000..d40bddbb9 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Commands/SyncedGrainCommandMiddlewareTests.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class SyncedGrainCommandMiddlewareTests + { + private readonly IStateFactory factory = A.Fake(); + private readonly MyGrain grain = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly SyncedGrainCommandMiddleware sut; + + public class MatchingCommand : MyCommand + { + } + + public SyncedGrainCommandMiddlewareTests() + { + A.CallTo(() => factory.GetSingleAsync(id)) + .Returns(grain); + + sut = new SyncedGrainCommandMiddleware(factory); + } + + [Fact] + public async Task Should_invoke_grain_when_command_is_correct() + { + var command = new MatchingCommand { AggregateId = id }; + var context = new CommandContext(command, A.Fake()); + + A.CallTo(() => grain.ExecuteAsync(command)) + .Returns(100); + + await sut.HandleAsync(context); + + Assert.Equal(100, context.Result()); + + A.CallTo(() => factory.Synchronize(id)) + .MustHaveHappened(); + A.CallTo(() => factory.Remove(id)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_remove_grain_from_cache_when_failed() + { + var command = new MatchingCommand { AggregateId = id }; + var context = new CommandContext(command, A.Fake()); + + A.CallTo(() => grain.ExecuteAsync(command)) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + + A.CallTo(() => factory.Synchronize(id)) + .MustNotHaveHappened(); + A.CallTo(() => factory.Remove(id)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_invoke_grain_when_command_is_not_correct() + { + var command = new MyCommand { AggregateId = id }; + var context = new CommandContext(command, A.Fake()); + + await sut.HandleAsync(context); + + Assert.Null(context.Result()); + + A.CallTo(() => grain.ExecuteAsync(command)) + .MustNotHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index bbd69a37f..1240ca92a 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -20,8 +20,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { public sealed class MyEventConsumerGrain : EventConsumerGrain { - public MyEventConsumerGrain(IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log) - : base(eventStore, eventDataFormatter, log) + public MyEventConsumerGrain(IStore store, IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log) + : base(store, eventStore, eventDataFormatter, log) { } @@ -53,8 +53,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains consumerName = eventConsumer.GetType().Name; - A.CallTo(() => store.WithSnapshots(consumerName, A>.Ignored)) - .Invokes(new Action>((key, a) => apply = a)) + A.CallTo(() => store.WithSnapshots(typeof(EventConsumerGrain), consumerName, A>.Ignored)) + .Invokes(new Action>((type, key, a) => apply = a)) .Returns(persistence); A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) @@ -71,7 +71,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); - sut = new MyEventConsumerGrain(eventStore, formatter, log); + sut = new MyEventConsumerGrain(store, eventStore, formatter, log); sutSubscriber = sut; } @@ -80,7 +80,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { state = state.Stopped(); - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); sut.Dispose(); @@ -93,7 +93,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_subscribe_to_event_store_when_not_found_in_db() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); sut.Dispose(); @@ -106,7 +106,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_subscribe_to_event_store_when_not_stopped_in_db() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); sut.Dispose(); @@ -119,7 +119,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_stop_subscription_when_stopped() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); sut.Stop(); sut.Stop(); @@ -138,7 +138,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_reset_consumer_when_resetting() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); sut.Stop(); sut.Reset(); @@ -165,7 +165,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_invoke_and_update_position_when_event_received() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); @@ -186,7 +186,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_ignore_old_events() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); A.CallTo(() => formatter.Parse(eventData, true)) @@ -210,7 +210,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); @@ -228,7 +228,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_stop_if_consumer_failed() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); var ex = new InvalidOperationException(); @@ -249,7 +249,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); var ex = new InvalidOperationException(); @@ -267,7 +267,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public void Should_stop_if_resetting_failed() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); var ex = new InvalidOperationException(); @@ -290,7 +290,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_stop_if_handling_failed() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); var ex = new InvalidOperationException(); @@ -321,7 +321,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_stop_if_deserialization_failed() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); var ex = new InvalidOperationException(); @@ -350,7 +350,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_start_after_stop_when_handling_failed() { - sut.ActivateAsync(consumerName, store).Wait(); + sut.ActivateAsync(consumerName).Wait(); sut.Activate(eventConsumer); var exception = new InvalidOperationException(); diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs index 9eed300b9..b305cab88 100644 --- a/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs @@ -13,7 +13,6 @@ using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.TestHelpers; using Xunit; @@ -21,48 +20,7 @@ namespace Squidex.Infrastructure.States { public class PersistenceEventSourcingTests { - private class MyStatefulObject : IStatefulObject - { - private readonly List appliedEvents = new List(); - private IPersistence persistence; - - public long ExpectedVersion { get; set; } = EtagVersion.Any; - - public List AppliedEvents - { - get { return appliedEvents; } - } - - public Task ActivateAsync(string key, IStore store) - { - persistence = store.WithEventSourcing(key, e => appliedEvents.Add(e.Payload)); - - return persistence.ReadAsync(ExpectedVersion); - } - - public Task WriteEventsAsync(params IEvent[] events) - { - return persistence.WriteEventsAsync(events.Select(Envelope.Create).ToArray()); - } - } - - private class MyStatefulObjectWithSnapshot : IStatefulObject - { - private IPersistence persistence; - - public long ExpectedVersion { get; set; } = EtagVersion.Any; - - public Task ActivateAsync(string key, IStore store) - { - persistence = store.WithSnapshotsAndEventSourcing(key, s => TaskHelper.Done, s => TaskHelper.Done); - - return persistence.ReadAsync(ExpectedVersion); - } - } - private readonly string key = Guid.NewGuid().ToString(); - private readonly MyStatefulObject statefulObject = new MyStatefulObject(); - private readonly MyStatefulObjectWithSnapshot statefulObjectWithSnapShot = new MyStatefulObjectWithSnapshot(); private readonly IEventDataFormatter eventDataFormatter = A.Fake(); private readonly IEventStore eventStore = A.Fake(); private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); @@ -70,42 +28,53 @@ namespace Squidex.Infrastructure.States private readonly IServiceProvider services = A.Fake(); private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); - private readonly StateFactory sut; + private readonly IStore sut; public PersistenceEventSourcingTests() { - A.CallTo(() => services.GetService(typeof(MyStatefulObject))) - .Returns(statefulObject); - A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot))) - .Returns(statefulObjectWithSnapShot); A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); - A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key)) - .Returns(key); - A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObjectWithSnapshot), key)) + A.CallTo(() => streamNameResolver.GetStreamName(typeof(object), key)) .Returns(key); - sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); - sut.Initialize(); + sut = new Store(eventStore, eventDataFormatter, services, streamNameResolver); } [Fact] public async Task Should_read_from_store() { - statefulObject.ExpectedVersion = 1; - var event1 = new MyEvent(); var event2 = new MyEvent(); SetupEventStore(event1, event2); - var actualObject = await sut.GetSingleAsync(key); + var persistedEvents = new List(); + var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + + await persistence.ReadAsync(); + + Assert.Equal(persistedEvents.ToArray(), new[] { event1, event2 }); + } + + [Fact] + public async Task Should_ignore_old_events() + { + var storedEvent = new StoredEvent("1", 0, new EventData()); + + A.CallTo(() => eventStore.QueryAsync(key, 0)) + .Returns(new List { storedEvent }); + + A.CallTo(() => eventDataFormatter.Parse(storedEvent.Data, true)) + .Throws(new TypeNameNotFoundException()); + + var persistedEvents = new List(); + var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); - Assert.Same(statefulObject, actualObject); - Assert.NotNull(cache.Get(key)); + await persistence.ReadAsync(); - Assert.Equal(actualObject.AppliedEvents, new[] { event1, event2 }); + Assert.Empty(persistedEvents); + Assert.Equal(0, persistence.Version); } [Fact] @@ -116,7 +85,11 @@ namespace Squidex.Infrastructure.States SetupEventStore(3, 2); - await sut.GetSingleAsync(key); + var persistedState = (object)null; + var persistedEvents = new List(); + var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + + await persistence.ReadAsync(); A.CallTo(() => eventStore.QueryAsync(key, 3)) .MustHaveHappened(); @@ -130,7 +103,11 @@ namespace Squidex.Infrastructure.States SetupEventStore(3, 0, 3); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + var persistedState = (object)null; + var persistedEvents = new List(); + var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + + await Assert.ThrowsAsync(() => persistence.ReadAsync()); } [Fact] @@ -141,50 +118,60 @@ namespace Squidex.Infrastructure.States SetupEventStore(3, 4, 3); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + var persistedState = (object)null; + var persistedEvents = new List(); + var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + + await Assert.ThrowsAsync(() => persistence.ReadAsync()); } [Fact] public async Task Should_throw_exception_if_not_found() { - statefulObject.ExpectedVersion = 0; - SetupEventStore(0); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + var persistedEvents = new List(); + var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + + await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } [Fact] public async Task Should_throw_exception_if_other_version_found() { - statefulObject.ExpectedVersion = 1; - SetupEventStore(3); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + var persistedEvents = new List(); + var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + + await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } [Fact] public async Task Should_throw_exception_if_other_version_found_from_snapshot() { - statefulObjectWithSnapShot.ExpectedVersion = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) .Returns((2, 2L)); SetupEventStore(0); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + var persistedState = (object)null; + var persistedEvents = new List(); + var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + + await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } [Fact] public async Task Should_not_throw_exception_if_nothing_expected() { - statefulObject.ExpectedVersion = EtagVersion.Any; - SetupEventStore(0); - await sut.GetSingleAsync(key); + var persistedState = (object)null; + var persistedEvents = new List(); + var persistence = sut.WithSnapshotsAndEventSourcing(key, x => persistedState = x, x => persistedEvents.Add(x.Payload)); + + await persistence.ReadAsync(); } [Fact] @@ -192,12 +179,13 @@ namespace Squidex.Infrastructure.States { SetupEventStore(3); - var actualObject = await sut.GetSingleAsync(key); + var persistedEvents = new List(); + var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); - Assert.Same(statefulObject, actualObject); + await persistence.ReadAsync(); - await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent()); - await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent()); + await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create)); + await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create)); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) .MustHaveHappened(); @@ -210,49 +198,15 @@ namespace Squidex.Infrastructure.States { SetupEventStore(3); - var actualObject = await sut.GetSingleAsync(key); + var persistedEvents = new List(); + var persistence = sut.WithEventSourcing(key, x => persistedEvents.Add(x.Payload)); + + await persistence.ReadAsync(); A.CallTo(() => eventStore.AppendAsync(A.Ignored, key, 2, A>.That.Matches(x => x.Count == 2))) .Throws(new WrongEventVersionException(1, 1)); - await Assert.ThrowsAsync(() => statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent())); - } - - [Fact] - public async Task Should_not_remove_from_cache_when_write_failed() - { - A.CallTo(() => eventStore.AppendAsync(A.Ignored, A.Ignored, A.Ignored, A>.Ignored)) - .Throws(new InvalidOperationException()); - - var actualObject = await sut.GetSingleAsync(key); - - await Assert.ThrowsAsync(() => statefulObject.WriteEventsAsync(new MyEvent())); - - Assert.True(cache.TryGetValue(key, out var t)); - } - - [Fact] - public async Task Should_return_same_instance_for_parallel_requests() - { - A.CallTo(() => snapshotStore.ReadAsync(key)) - .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => ((object)1, 1L))); - - var tasks = new List>(); - - for (var i = 0; i < 1000; i++) - { - tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); - } - - var retrievedStates = await Task.WhenAll(tasks); - - foreach (var retrievedState in retrievedStates) - { - Assert.Same(retrievedStates[0], retrievedState); - } - - A.CallTo(() => eventStore.QueryAsync(key, 0)) - .MustHaveHappened(Repeated.Exactly.Once); + await Assert.ThrowsAsync(() => persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create))); } private void SetupEventStore(int count, int eventOffset = 0, int readPosition = 0) diff --git a/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs b/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs index c64720e89..b93e483f6 100644 --- a/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; @@ -18,45 +17,9 @@ using Xunit; namespace Squidex.Infrastructure.States { - public class PersistenceSnapshotTests : IDisposable + public class PersistenceSnapshotTests { - private class MyStatefulObject : IStatefulObject - { - private IPersistence persistence; - private int state; - - public long ExpectedVersion { get; set; } = EtagVersion.Any; - - public long Version - { - get { return persistence.Version; } - } - - public int State - { - get { return state; } - } - - public Task ActivateAsync(string key, IStore store) - { - persistence = store.WithSnapshots(key, s => state = s); - - return persistence.ReadAsync(ExpectedVersion); - } - - public void SetState(int value) - { - state = value; - } - - public Task WriteStateAsync() - { - return persistence.WriteSnapshotAsync(state); - } - } - private readonly string key = Guid.NewGuid().ToString(); - private readonly MyStatefulObject statefulObject = new MyStatefulObject(); private readonly IEventDataFormatter eventDataFormatter = A.Fake(); private readonly IEventStore eventStore = A.Fake(); private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); @@ -64,99 +27,101 @@ namespace Squidex.Infrastructure.States private readonly IServiceProvider services = A.Fake(); private readonly ISnapshotStore snapshotStore = A.Fake>(); private readonly IStreamNameResolver streamNameResolver = A.Fake(); - private readonly StateFactory sut; + private readonly IStore sut; public PersistenceSnapshotTests() { - A.CallTo(() => services.GetService(typeof(MyStatefulObject))) - .Returns(statefulObject); A.CallTo(() => services.GetService(typeof(ISnapshotStore))) .Returns(snapshotStore); - sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); - sut.Initialize(); - } - - public void Dispose() - { - sut.Dispose(); + sut = new Store(eventStore, eventDataFormatter, services, streamNameResolver); } [Fact] public async Task Should_read_from_store() { - statefulObject.ExpectedVersion = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((123, 1)); + .Returns((20, 10)); - var actualObject = await sut.GetSingleAsync(key); + var persistedState = 0; + var persistence = sut.WithSnapshots(key, x => persistedState = x); - Assert.Same(statefulObject, actualObject); - Assert.NotNull(cache.Get(key)); + await persistence.ReadAsync(); - Assert.Equal(123, statefulObject.State); + Assert.Equal(10, persistence.Version); + Assert.Equal(20, persistedState); } [Fact] - public async Task Should_set_to_empty_when_store_returns_not_found() + public async Task Should_return_empty_version_when_version_negative() { A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((123, EtagVersion.NotFound)); + .Returns((20, -10)); + + var persistedState = 0; + var persistence = sut.WithSnapshots(key, x => persistedState = x); - var actualObject = await sut.GetSingleAsync(key); + await persistence.ReadAsync(); - Assert.Equal(-1, statefulObject.Version); - Assert.Equal( 0, statefulObject.State); + Assert.Equal(EtagVersion.Empty, persistence.Version); } [Fact] - public async Task Should_throw_exception_if_not_found() + public async Task Should_set_to_empty_when_store_returns_not_found() { - statefulObject.ExpectedVersion = 0; - A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((0, EtagVersion.Empty)); + .Returns((20, EtagVersion.Empty)); + + var persistedState = 0; + var persistence = sut.WithSnapshots(key, x => persistedState = x); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + await persistence.ReadAsync(); + + Assert.Equal(-1, persistence.Version); + Assert.Equal( 0, persistedState); } [Fact] - public async Task Should_throw_exception_if_other_version_found() + public async Task Should_throw_exception_if_not_found_and_version_expected() { - statefulObject.ExpectedVersion = 1; - A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((2, 2)); + .Returns((123, EtagVersion.Empty)); + + var persistedState = 0; + var persistence = sut.WithSnapshots(key, x => persistedState = x); - await Assert.ThrowsAsync(() => sut.GetSingleAsync(key)); + await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } [Fact] - public async Task Should_not_throw_exception_if_noting_expected() + public async Task Should_throw_exception_if_other_version_found() { A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((0, EtagVersion.Empty)); + .Returns((123, 2)); - await sut.GetSingleAsync(key); + var persistedState = 0; + var persistence = sut.WithSnapshots(key, x => persistedState = x); + + await Assert.ThrowsAsync(() => persistence.ReadAsync(1)); } [Fact] public async Task Should_write_to_store_with_previous_version() { A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((123, 13)); + .Returns((20, 10)); - var actualObject = await sut.GetSingleAsync(key); + var persistedState = 0; + var persistence = sut.WithSnapshots(key, x => persistedState = x); - Assert.Same(statefulObject, actualObject); - Assert.Equal(123, statefulObject.State); + await persistence.ReadAsync(); - statefulObject.SetState(456); + Assert.Equal(10, persistence.Version); + Assert.Equal(20, persistedState); - await statefulObject.WriteStateAsync(); + await persistence.WriteSnapshotAsync(100); - A.CallTo(() => snapshotStore.WriteAsync(key, 456, 13, 14)) + A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11)) .MustHaveHappened(); } @@ -164,51 +129,17 @@ namespace Squidex.Infrastructure.States public async Task Should_wrap_exception_when_writing_to_store_with_previous_version() { A.CallTo(() => snapshotStore.ReadAsync(key)) - .Returns((123, 13)); + .Returns((20, 10)); - A.CallTo(() => snapshotStore.WriteAsync(key, 123, 13, 14)) + A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11)) .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); - var actualObject = await sut.GetSingleAsync(key); + var persistedState = 0; + var persistence = sut.WithSnapshots(key, x => persistedState = x); - await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); - } + await persistence.ReadAsync(); - [Fact] - public async Task Should_not_remove_from_cache_when_write_failed() - { - A.CallTo(() => snapshotStore.WriteAsync(A.Ignored, A.Ignored, A.Ignored, A.Ignored)) - .Throws(new InvalidOperationException()); - - var actualObject = await sut.GetSingleAsync(key); - - await Assert.ThrowsAsync(() => statefulObject.WriteStateAsync()); - - Assert.True(cache.TryGetValue(key, out var t)); - } - - [Fact] - public async Task Should_return_same_instance_for_parallel_requests() - { - A.CallTo(() => snapshotStore.ReadAsync(key)) - .ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L))); - - var tasks = new List>(); - - for (var i = 0; i < 1000; i++) - { - tasks.Add(Task.Run(() => sut.GetSingleAsync(key))); - } - - var retrievedStates = await Task.WhenAll(tasks); - - foreach (var retrievedState in retrievedStates) - { - Assert.Same(retrievedStates[0], retrievedState); - } - - A.CallTo(() => snapshotStore.ReadAsync(key)) - .MustHaveHappened(Repeated.Exactly.Once); + await Assert.ThrowsAsync(() => persistence.WriteSnapshotAsync(100)); } } } \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs index e772b3b7b..8e540cf17 100644 --- a/tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Tasks; using Xunit; @@ -22,7 +21,7 @@ namespace Squidex.Infrastructure.States { private class MyStatefulObject : IStatefulObject { - public Task ActivateAsync(string key, IStore store) + public Task ActivateAsync(string key) { return TaskHelper.Done; } @@ -30,23 +29,17 @@ namespace Squidex.Infrastructure.States private readonly string key = Guid.NewGuid().ToString(); private readonly MyStatefulObject statefulObject = new MyStatefulObject(); - private readonly IEventDataFormatter eventDataFormatter = A.Fake(); - private readonly IEventStore eventStore = A.Fake(); private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IPubSub pubSub = new InMemoryPubSub(true); private readonly IServiceProvider services = A.Fake(); - private readonly ISnapshotStore snapshotStore = A.Fake>(); - private readonly IStreamNameResolver streamNameResolver = A.Fake(); private readonly StateFactory sut; public StateFactoryTests() { A.CallTo(() => services.GetService(typeof(MyStatefulObject))) .Returns(statefulObject); - A.CallTo(() => services.GetService(typeof(ISnapshotStore))) - .Returns(snapshotStore); - sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver); + sut = new StateFactory(pubSub, cache, services); sut.Initialize(); } diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs index fff8348a3..274d85baf 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs @@ -11,11 +11,11 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Infrastructure.TestHelpers { - internal sealed class MyCommand : IAggregateCommand, ITimestampCommand + public class MyCommand : IAggregateCommand, ITimestampCommand { public Guid AggregateId { get; set; } - public long ExpectedVersion { get; set; } + public long ExpectedVersion { get; set; } = EtagVersion.Any; public Instant Timestamp { get; set; } } diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs deleted file mode 100644 index 0b28e17b3..000000000 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Commands; - -namespace Squidex.Infrastructure.TestHelpers -{ - internal sealed class MyDomainObject : DomainObjectBase - { - } -} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs new file mode 100644 index 000000000..4a50e4744 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.TestHelpers +{ + public class MyGrain : DomainObjectGrain + { + public MyGrain(IStore store) + : base(store) + { + } + + public override Task ExecuteAsync(IAggregateCommand command) + { + return Task.FromResult(null); + } + } +} diff --git a/tools/Migrate_01/Migrations/AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs index 68aa6e9dd..8b1724c92 100644 --- a/tools/Migrate_01/Migrations/AddPatterns.cs +++ b/tools/Migrate_01/Migrations/AddPatterns.cs @@ -34,7 +34,7 @@ namespace Migrate_01.Migrations foreach (var id in ids) { - var app = await stateFactory.GetSingleAsync(id); + var app = await stateFactory.GetSingleAsync(id); if (app.Snapshot.Patterns.Count == 0) { @@ -51,10 +51,8 @@ namespace Migrate_01.Migrations Message = pattern.Message }; - app.AddPattern(command); + await app.ExecuteAsync(command); } - - await app.WriteAsync(); } } } diff --git a/tools/Migrate_01/Rebuilder.cs b/tools/Migrate_01/Rebuilder.cs index a4a350b9b..af94ccfa6 100644 --- a/tools/Migrate_01/Rebuilder.cs +++ b/tools/Migrate_01/Rebuilder.cs @@ -79,7 +79,7 @@ namespace Migrate_01 { if (@event.Payload is AssetEvent assetEvent && handledIds.Add(assetEvent.AssetId)) { - var asset = await stateFactory.CreateAsync(assetEvent.AssetId); + var asset = await stateFactory.CreateAsync(assetEvent.AssetId); asset.ApplySnapshot(asset.Snapshot.Apply(@event)); @@ -107,19 +107,19 @@ namespace Migrate_01 { if (@event.Payload is SchemaEvent schemaEvent && handledIds.Add(schemaEvent.SchemaId.Id)) { - var schema = await stateFactory.GetSingleAsync(schemaEvent.SchemaId.Id); + var schema = await stateFactory.GetSingleAsync(schemaEvent.SchemaId.Id); await schema.WriteSnapshotAsync(); } else if (@event.Payload is RuleEvent ruleEvent && handledIds.Add(ruleEvent.RuleId)) { - var rule = await stateFactory.GetSingleAsync(ruleEvent.RuleId); + var rule = await stateFactory.GetSingleAsync(ruleEvent.RuleId); await rule.WriteSnapshotAsync(); } else if (@event.Payload is AppEvent appEvent && handledIds.Add(appEvent.AppId.Id)) { - var app = await stateFactory.GetSingleAsync(appEvent.AppId.Id); + var app = await stateFactory.GetSingleAsync(appEvent.AppId.Id); await app.WriteSnapshotAsync(); }