diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 5a3a7f1d3..ccdf24331 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -138,6 +138,36 @@ namespace Squidex.Domain.Apps.Entities.Apps }); } + protected Task On(AddPattern command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppPattern.CanAdd(a.State.Patterns, command); + + a.AddPattern(command); + }); + } + + protected Task On(DeletePattern command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppPattern.CanDelete(a.State.Patterns, command); + + a.DeletePattern(command); + }); + } + + protected async Task On(UpdatePattern command, CommandContext context) + { + await handler.UpdateAsync(context, a => + { + GuardAppPattern.CanUpdate(a.State.Patterns, command); + + a.UpdatePattern(command); + }); + } + protected Task On(ChangePlan command, CommandContext context) { return handler.UpdateSyncedAsync(context, async a => diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index d11519917..6aae7ccfe 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -123,6 +123,33 @@ namespace Squidex.Domain.Apps.Entities.Apps 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) diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs new file mode 100644 index 000000000..7432c4d81 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// AddPattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class AddPattern : AppAggregateCommand + { + public Guid Id { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string DefaultMessage { get; set; } + + public AddPattern() + { + Id = Guid.NewGuid(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs index eb173e4f5..57e1d18db 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs @@ -13,10 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { public sealed class CreateApp : SquidexCommand, IAggregateCommand { - public string Name { get; set; } - public Guid AppId { get; set; } + public string Name { get; set; } + Guid IAggregateCommand.AggregateId { get { return AppId; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs new file mode 100644 index 000000000..d152f40d0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// DeletePattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class DeletePattern : AppAggregateCommand + { + public Guid Id { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs new file mode 100644 index 000000000..20a436ba3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// UpdatePattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class UpdatePattern : AppAggregateCommand + { + public Guid Id { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string DefaultMessage { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs new file mode 100644 index 000000000..3aea26e1c --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// GuardAppPattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppPattern + { + public static void CanAdd(AppPatterns patterns, AddPattern command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot add pattern.", error => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + error(new ValidationError("Pattern name can not be empty.", nameof(command.Name))); + } + + if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) + { + error(new ValidationError("Pattern name is already assigned.", nameof(command.Name))); + } + + if (string.IsNullOrWhiteSpace(command.Pattern)) + { + error(new ValidationError("Pattern can not be empty.", nameof(command.Pattern))); + } + else if (!command.Pattern.IsValidRegex()) + { + error(new ValidationError("Pattern is not a valid regular expression.", nameof(command.Pattern))); + } + + if (patterns.Values.Any(x => x.Pattern == command.Pattern)) + { + error(new ValidationError("Pattern already exists.", nameof(command.Pattern))); + } + }); + } + + public static void CanDelete(AppPatterns patterns, DeletePattern command) + { + Guard.NotNull(command, nameof(command)); + + if (!patterns.ContainsKey(command.Id)) + { + throw new DomainObjectNotFoundException(command.Id.ToString(), typeof(AppPattern)); + } + } + + public static void CanUpdate(AppPatterns patterns, UpdatePattern command) + { + Guard.NotNull(command, nameof(command)); + + if (!patterns.ContainsKey(command.Id)) + { + throw new DomainObjectNotFoundException(command.Id.ToString(), typeof(AppPattern)); + } + + Validate.It(() => "Cannot update pattern.", error => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + error(new ValidationError("Pattern name can not be empty.", nameof(command.Name))); + } + + if (patterns.Any(x => x.Key != command.Id && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) + { + error(new ValidationError("Pattern name is already assigned.", nameof(command.Name))); + } + + if (string.IsNullOrWhiteSpace(command.Pattern)) + { + error(new ValidationError("Pattern can not be empty.", nameof(command.Pattern))); + } + else if (!command.Pattern.IsValidRegex()) + { + error(new ValidationError("Pattern is not a valid regular expression.", nameof(command.Pattern))); + } + + if (patterns.Any(x => x.Key != command.Id && x.Value.Pattern == command.Pattern)) + { + error(new ValidationError("Pattern already exists.", nameof(command.Pattern))); + } + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs index 154478f1e..85912cac2 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -18,6 +18,8 @@ namespace Squidex.Domain.Apps.Entities.Apps AppClients Clients { get; } + AppPatterns Patterns { get; } + AppContributors Contributors { get; } LanguagesConfig LanguagesConfig { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 35ed457a5..917140969 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -31,6 +31,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.State [JsonProperty] public AppClients Clients { get; set; } = AppClients.Empty; + [JsonProperty] + public AppPatterns Patterns { get; set; } = AppPatterns.Empty; + [JsonProperty] public AppContributors Contributors { get; set; } = AppContributors.Empty; @@ -77,6 +80,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.State Clients = Clients.Revoke(@event.Id); } + protected void On(AppPatternAdded @event) + { + Patterns = Patterns.Add(@event.Id, @event.Name, @event.Pattern, @event.DefaultMessage); + } + + protected void On(AppPatternDeleted @event) + { + Patterns = Patterns.Remove(@event.Id); + } + + protected void On(AppPatternUpdated @event) + { + Patterns = Patterns.Update(@event.Id, @event.Name, @event.Pattern, @event.DefaultMessage); + } + protected void On(AppLanguageAdded @event) { LanguagesConfig = LanguagesConfig.Set(new LanguageConfig(@event.Language)); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs index fae8ac4f3..957df0e38 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Linq; using FluentAssertions; using Squidex.Domain.Apps.Core.Apps; using Xunit; @@ -25,8 +24,6 @@ namespace Squidex.Domain.Apps.Core.Model.Apps public AppPatternsTests() { defaultPatterns = AppPatterns.Empty.Add(firstId, "Default", "Default Pattern", "Message"); - - id = Guid.NewGuid(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs index bb778526d..b07f64b1a 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs @@ -26,10 +26,11 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly IAppPlansProvider appPlansProvider = A.Fake(); private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); private readonly IUserResolver userResolver = A.Fake(); - private readonly AppDomainObject app = new AppDomainObject(); 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(); private readonly AppCommandMiddleware sut; protected override Guid Id @@ -232,6 +233,47 @@ namespace Squidex.Domain.Apps.Entities.Apps }); } + [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 { Id = patternId, Name = "Any", Pattern = "." })); + + var context = CreateContextForCommand(new UpdatePattern { Id = 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 { Id = patternId, Name = "Any", Pattern = "." })); + + var context = CreateContextForCommand(new DeletePattern { Id = patternId }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + private AppDomainObject CreateApp() { app.Create(CreateCommand(new CreateApp { AppId = AppId, Name = AppName })); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index bea62e007..65bc15461 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -24,6 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly string clientId = "client"; private readonly string clientNewName = "My Client"; private readonly string planId = "premium"; + private readonly Guid patternId = Guid.NewGuid(); private readonly AppDomainObject sut = new AppDomainObject(); protected override Guid Id @@ -281,6 +282,83 @@ namespace Squidex.Domain.Apps.Entities.Apps ); } + [Fact] + public void AddPattern_should_throw_exception_if_app_not_created() + { + Assert.Throws(() => sut.AddPattern(CreateCommand(new AddPattern { Id = patternId, Name = "Any", Pattern = ".*" }))); + } + + [Fact] + public void AddPattern_should_create_events() + { + CreateApp(); + + sut.AddPattern(CreateCommand(new AddPattern { Id = patternId, Name = "Any", Pattern = ".*", DefaultMessage = "Msg" })); + + Assert.Single(sut.State.Patterns); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppPatternAdded { Id = patternId, Name = "Any", Pattern = ".*", DefaultMessage = "Msg" }) + ); + } + + [Fact] + public void DeletePattern_should_throw_exception_if_app_not_created() + { + Assert.Throws(() => + { + sut.DeletePattern(CreateCommand(new DeletePattern + { + Id = Guid.NewGuid() + })); + }); + } + + [Fact] + public void DeletePattern_should_create_events() + { + CreateApp(); + CreatePattern(); + + sut.DeletePattern(CreateCommand(new DeletePattern { Id = patternId })); + + Assert.Empty(sut.State.Patterns); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppPatternDeleted { Id = patternId }) + ); + } + + [Fact] + public void UpdatePattern_should_throw_exception_if_app_not_created() + { + Assert.Throws(() => sut.UpdatePattern(CreateCommand(new UpdatePattern { Id = patternId, Name = "Any", Pattern = ".*" }))); + } + + [Fact] + public void UpdatePattern_should_create_events() + { + CreateApp(); + CreatePattern(); + + sut.UpdatePattern(CreateCommand(new UpdatePattern { Id = patternId, Name = "Any", Pattern = ".*", DefaultMessage = "Msg" })); + + Assert.Single(sut.State.Patterns); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppPatternUpdated { Id = patternId, Name = "Any", Pattern = ".*", DefaultMessage = "Msg" }) + ); + } + + private void CreatePattern() + { + sut.AddPattern(CreateCommand(new AddPattern { Id = patternId, Name = "Name", Pattern = ".*" })); + sut.ClearUncommittedEvents(); + } + private void CreateApp() { sut.Create(CreateCommand(new CreateApp { Name = AppName })); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs new file mode 100644 index 000000000..e6f3202ef --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs @@ -0,0 +1,169 @@ +// ========================================================================== +// GuardAppPatternsTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Apps.Guards; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable SA1310 // Field names must not contain underscore + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public class GuardAppPatternsTests + { + private readonly Guid patternId = Guid.NewGuid(); + private readonly AppPatterns patterns_0 = AppPatterns.Empty; + + [Fact] + public void CanAdd_should_throw_exception_if_name_empty() + { + var command = new AddPattern { Id = patternId, Name = string.Empty, Pattern = ".*" }; + + Assert.Throws(() => GuardAppPattern.CanAdd(patterns_0, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_id_empty_guid() + { + var command = new AddPattern { Name = string.Empty, Pattern = ".*" }; + + Assert.Throws(() => GuardAppPattern.CanAdd(patterns_0, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_pattern_empty() + { + var command = new AddPattern { Id = patternId, Name = "any", Pattern = string.Empty }; + + Assert.Throws(() => GuardAppPattern.CanAdd(patterns_0, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_pattern_not_valid() + { + var command = new AddPattern { Id = patternId, Name = "any", Pattern = "[0-9{1}" }; + + Assert.Throws(() => GuardAppPattern.CanAdd(patterns_0, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_exists() + { + var patterns_1 = patterns_0.Add(Guid.NewGuid(), "any", "[a-z]", "Message"); + + var command = new AddPattern { Id = patternId, Name = "any", Pattern = ".*" }; + + Assert.Throws(() => GuardAppPattern.CanAdd(patterns_1, command)); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_success() + { + var command = new AddPattern { Id = patternId, Name = "any", Pattern = ".*" }; + + GuardAppPattern.CanAdd(patterns_0, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_pattern_not_found() + { + var command = new DeletePattern { Id = patternId }; + + Assert.Throws(() => GuardAppPattern.CanDelete(patterns_0, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_success() + { + var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message"); + + var command = new DeletePattern { Id = patternId }; + + GuardAppPattern.CanDelete(patterns_1, command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_name_empty() + { + var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message"); + + var command = new UpdatePattern { Id = patternId, Name = string.Empty, Pattern = ".*" }; + + Assert.Throws(() => GuardAppPattern.CanUpdate(patterns_1, command)); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_pattern_empty() + { + var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message"); + + var command = new UpdatePattern { Id = patternId, Name = "any", Pattern = string.Empty }; + + Assert.Throws(() => GuardAppPattern.CanUpdate(patterns_1, command)); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_pattern_not_valid() + { + var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message"); + + var command = new UpdatePattern { Id = patternId, Name = "any", Pattern = "[0-9{1}" }; + + Assert.Throws(() => GuardAppPattern.CanUpdate(patterns_1, command)); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_name_exists() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var patterns_1 = patterns_0.Add(id1, "Pattern1", "[0-5]", "Message"); + var patterns_2 = patterns_1.Add(id2, "Pattern2", "[0-4]", "Message"); + + var command = new UpdatePattern { Id = id2, Name = "Pattern1", Pattern = "[0-4]" }; + + Assert.Throws(() => GuardAppPattern.CanUpdate(patterns_2, command)); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_pattern_exists() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var patterns_1 = patterns_0.Add(id1, "Pattern1", "[0-5]", "Message"); + var patterns_2 = patterns_1.Add(id2, "Pattern2", "[0-4]", "Message"); + + var command = new UpdatePattern { Id = id2, Name = "Pattern2", Pattern = "[0-5]" }; + + Assert.Throws(() => GuardAppPattern.CanUpdate(patterns_2, command)); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_pattern_does_not_exists() + { + var command = new UpdatePattern { Id = patternId, Name = "Pattern1", Pattern = ".*" }; + + Assert.Throws(() => GuardAppPattern.CanUpdate(patterns_0, command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_pattern_exist_with_valid_command() + { + var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message"); + + var command = new UpdatePattern { Id = patternId, Name = "Pattern1", Pattern = ".*" }; + + GuardAppPattern.CanUpdate(patterns_1, command); + } + } +}