diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs index 2ffadc99d..3675f1b81 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs @@ -34,11 +34,11 @@ namespace Squidex.Domain.Apps.Core.Contents } [Pure] - public Workflows Add(string name) + public Workflows Add(Guid workflowId, string name) { Guard.NotNullOrEmpty(name, nameof(name)); - return new Workflows(With(Guid.NewGuid(), Workflow.CreateDefault(name))); + return new Workflows(With(workflowId, Workflow.CreateDefault(name))); } [Pure] @@ -54,6 +54,11 @@ namespace Squidex.Domain.Apps.Core.Contents { Guard.NotNull(workflow, nameof(workflow)); + if (id == Guid.Empty) + { + return Set(workflow); + } + if (!ContainsKey(id)) { return this; diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs index 3b70a4c68..54ca7b4bb 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs @@ -5,10 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; + namespace Squidex.Domain.Apps.Entities.Apps.Commands { public sealed class AddWorkflow : AppCommand { + public Guid WorkflowId { get; set; } + public string Name { get; set; } + + public AddWorkflow() + { + WorkflowId = Guid.NewGuid(); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs index b802d55ef..738b2f70a 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs @@ -14,11 +14,26 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { public static class GuardAppWorkflows { + public static void CanAdd(AddWorkflow command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot add workflow.", e => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + e(Not.Defined("Name"), nameof(command.Name)); + } + }); + } + public static void CanUpdate(Workflows workflows, UpdateWorkflow command) { Guard.NotNull(command, nameof(command)); - Validate.It(() => "Cannot configure workflow.", e => + GetWorkflowOrThrow(workflows, command.WorkflowId); + + Validate.It(() => "Cannot update workflow.", e => { if (command.Workflow == null) { @@ -74,14 +89,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards }); } - internal static void CanAdd(AddWorkflow c) + public static void CanDelete(Workflows workflows, DeleteWorkflow command) { - throw new NotImplementedException(); + Guard.NotNull(command, nameof(command)); + + GetWorkflowOrThrow(workflows, command.WorkflowId); } - internal static void CanDelete(Workflows workflows, DeleteWorkflow c) + private static Workflow GetWorkflowOrThrow(Workflows workflows, Guid id) { - throw new NotImplementedException(); + if (!workflows.TryGetValue(id, out var workflow)) + { + throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity)); + } + + return workflow; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index ac71df0bc..7e870d56c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -96,9 +96,19 @@ namespace Squidex.Domain.Apps.Entities.Apps.State Clients = Clients.Revoke(@event.Id); } - protected void On(AppWorkflowConfigured @event) + protected void On(AppWorkflowAdded @event) { - Workflows = Workflows.Set(@event.Workflow); + Workflows = Workflows.Add(@event.WorkflowId, @event.Name); + } + + protected void On(AppWorkflowUpdated @event) + { + Workflows = Workflows.Update(@event.WorkflowId, @event.Workflow); + } + + protected void On(AppWorkflowDeleted @event) + { + Workflows = Workflows.Remove(@event.WorkflowId); } protected void On(AppPatternAdded @event) diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs index 728b04b83..3e5627bee 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs +++ b/src/Squidex.Domain.Apps.Events/Apps/AppWorkflowAdded.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Apps @@ -12,6 +13,8 @@ namespace Squidex.Domain.Apps.Events.Apps [EventType(nameof(AppWorkflowAdded))] public sealed class AppWorkflowAdded : AppEvent { + public Guid WorkflowId { get; set; } + public string Name { get; set; } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 811dd97ef..843c2e424 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs @@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var clients_1 = clients_0.Revoke("2"); - Assert.Empty(clients_1); + Assert.NotEmpty(clients_1); } } } 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 5e22067b7..de159e090 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs @@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var patterns_1 = patterns_0.Remove(id); - Assert.Empty(patterns_1); + Assert.NotEmpty(patterns_1); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index ac89aa8ce..591708388 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var roles_1 = roles_0.Remove(role); - Assert.Empty(roles_1); + Assert.NotEmpty(roles_1); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs index c81db61cb..8a3c485e8 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowsTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Linq; using Squidex.Domain.Apps.Core.Contents; using Xunit; @@ -38,20 +37,32 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_add_new_workflow_with_default_states() { - var workflows_1 = workflows_0.Add("1"); + var id = Guid.NewGuid(); - Assert.Equal(workflows_1.GetFirst().Steps.Keys, new[] { Status.Archived, Status.Draft, Status.Published }); + var workflows_1 = workflows_0.Add(id, "1"); + + Assert.Equal(workflows_1[id].Steps.Keys, new[] { Status.Archived, Status.Draft, Status.Published }); } [Fact] public void Should_update_workflow() { - var workflows_1 = workflows_0.Add("1"); - var workflows_2 = workflows_1.Update(workflows_1.Keys.First(), Workflow.Empty); + var id = Guid.NewGuid(); + + var workflows_1 = workflows_0.Add(id, "1"); + var workflows_2 = workflows_1.Update(id, Workflow.Empty); Assert.Empty(workflows_2.GetFirst().Steps.Keys); } + [Fact] + public void Should_update_workflow_with_default_guid() + { + var workflows_1 = workflows_0.Update(Guid.Empty, Workflow.Empty); + + Assert.NotEmpty(workflows_1); + } + [Fact] public void Should_do_nothing_if_workflow_to_update_not_found() { @@ -63,8 +74,10 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_remove_workflow() { - var workflows_1 = workflows_0.Add("1"); - var workflows_2 = workflows_1.Remove(workflows_1.Keys.First()); + var id = Guid.NewGuid(); + + var workflows_1 = workflows_0.Add(id, "1"); + var workflows_2 = workflows_1.Remove(id); Assert.Empty(workflows_2); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs index a584ef196..6d3ac1bb3 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -38,6 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly string planIdPaid = "premium"; private readonly string planIdFree = "free"; private readonly AppGrain sut; + private readonly Guid workflowId = Guid.NewGuid(); private readonly Guid patternId1 = Guid.NewGuid(); private readonly Guid patternId2 = Guid.NewGuid(); private readonly Guid patternId3 = Guid.NewGuid(); @@ -299,9 +300,9 @@ namespace Squidex.Domain.Apps.Entities.Apps } [Fact] - public async Task ConfigureWorkflow_should_create_events_and_update_state() + public async Task AddWorkflow_should_create_events_and_update_state() { - var command = new UpdateWorkflow { Workflow = Workflow.Default }; + var command = new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" }; await ExecuteCreateAsync(); @@ -313,7 +314,47 @@ namespace Squidex.Domain.Apps.Entities.Apps LastEvents .ShouldHaveSameEvents( - CreateEvent(new AppWorkflowConfigured { Workflow = Workflow.Default }) + CreateEvent(new AppWorkflowAdded { WorkflowId = workflowId, Name = "my-workflow" }) + ); + } + + [Fact] + public async Task UpdateWorkflow_should_create_events_and_update_state() + { + var command = new UpdateWorkflow { WorkflowId = workflowId, Workflow = Workflow.Default }; + + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.NotEmpty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowUpdated { WorkflowId = workflowId, Workflow = Workflow.Default }) + ); + } + + [Fact] + public async Task DeleteWorkflow_should_create_events_and_update_state() + { + var command = new DeleteWorkflow { WorkflowId = workflowId }; + + await ExecuteCreateAsync(); + await ExecuteAddWorkflowAsync(); + + var result = await sut.ExecuteAsync(CreateCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Empty(sut.Snapshot.Workflows); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new AppWorkflowDeleted { WorkflowId = workflowId }) ); } @@ -540,6 +581,11 @@ namespace Squidex.Domain.Apps.Entities.Apps return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language })); } + private Task ExecuteAddWorkflowAsync() + { + return sut.ExecuteAsync(CreateCommand(new AddWorkflow { WorkflowId = workflowId, Name = "my-workflow" })); + } + private Task ExecuteChangePlanAsync() { return sut.ExecuteAsync(CreateCommand(new ChangePlan { PlanId = planIdPaid })); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs index 3bb1902c1..3431e88ce 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppPatternsTests.cs @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanAdd_should_not_throw_exception_if_success() + public void CanAdd_should_not_throw_exception_if_command_is_valid() { var command = new AddPattern { PatternId = patternId, Name = "any", Pattern = ".*" }; @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanDelete_should_not_throw_exception_if_success() + public void CanDelete_should_not_throw_exception_if_command_is_valid() { var patterns_1 = patterns_0.Add(patternId, "any", ".*", "Message"); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs index bd16881f7..c469ac3f4 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanAdd_should_not_throw_exception_if_success() + public void CanAdd_should_not_throw_exception_if_command_is_valid() { var command = new AddRole { Name = roleName }; @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards } [Fact] - public void CanDelete_should_not_throw_exception_if_success() + public void CanDelete_should_not_throw_exception_if_command_is_valid() { var roles_1 = roles_0.Add(roleName); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs index ce724c8c0..d348f7014 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Apps.Commands; @@ -16,17 +17,54 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { public class GuardAppWorkflowTests { + private readonly Guid workflowId = Guid.NewGuid(); + private readonly Workflows workflows; + + public GuardAppWorkflowTests() + { + workflows = Workflows.Empty.Add(workflowId, "name"); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_is_not_defined() + { + var command = new AddWorkflow(); + + ValidationAssert.Throws(() => GuardAppWorkflows.CanAdd(command), + new ValidationError("Name is required.", "Name")); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_command_is_valid() + { + var command = new AddWorkflow { Name = "my-workflow" }; + + GuardAppWorkflows.CanAdd(command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_workflow_not_found() + { + var command = new UpdateWorkflow + { + Workflow = Workflow.Empty, + WorkflowId = Guid.NewGuid() + }; + + Assert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command)); + } + [Fact] - public void CanConfigure_should_throw_exception_if_workflow_is_not_defined() + public void CanUpdate_should_throw_exception_if_workflow_is_not_defined() { - var command = new UpdateWorkflow(); + var command = new UpdateWorkflow { WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Workflow is required.", "Workflow")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_has_no_initial_step() + public void CanUpdate_should_throw_exception_if_workflow_has_no_initial_step() { var command = new UpdateWorkflow { @@ -35,15 +73,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { [Status.Published] = new WorkflowStep() }, - default) + default), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Initial step is required.", "Workflow.Initial")); } [Fact] - public void CanConfigure_should_throw_exception_if_initial_step_is_published() + public void CanUpdate_should_throw_exception_if_initial_step_is_published() { var command = new UpdateWorkflow { @@ -52,15 +91,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { [Status.Published] = new WorkflowStep() }, - Status.Published) + Status.Published), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Initial step cannot be published step.", "Workflow.Initial")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_does_not_have_published_state() + public void CanUpdate_should_throw_exception_if_workflow_does_not_have_published_state() { var command = new UpdateWorkflow { @@ -69,15 +109,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards { [Status.Draft] = new WorkflowStep() }, - Status.Draft) + Status.Draft), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Workflow must have a published step.", "Workflow.Steps")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_step_is_not_defined() + public void CanUpdate_should_throw_exception_if_workflow_step_is_not_defined() { var command = new UpdateWorkflow { @@ -87,15 +128,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Status.Published] = null, [Status.Draft] = new WorkflowStep() }, - Status.Draft) + Status.Draft), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Step is required.", "Workflow.Steps.Published")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_transition_is_invalid() + public void CanUpdate_should_throw_exception_if_workflow_transition_is_invalid() { var command = new UpdateWorkflow { @@ -110,15 +152,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards }), [Status.Draft] = new WorkflowStep() }, - Status.Draft) + Status.Draft), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Archived")); } [Fact] - public void CanConfigure_should_throw_exception_if_workflow_transition_is_not_defined() + public void CanUpdate_should_throw_exception_if_workflow_transition_is_not_defined() { var command = new UpdateWorkflow { @@ -134,19 +177,36 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards [Status.Draft] = null }) }, - Status.Draft) + Status.Draft), + WorkflowId = workflowId }; - ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(command), + ValidationAssert.Throws(() => GuardAppWorkflows.CanUpdate(workflows, command), new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); } [Fact] - public void CanConfigure_should_not_throw_exception_if_workflow_is_valid() + public void CanUpdate_should_not_throw_exception_if_workflow_is_valid() + { + var command = new UpdateWorkflow { Workflow = Workflow.Default, WorkflowId = workflowId }; + + GuardAppWorkflows.CanUpdate(workflows, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_workflow_not_found() + { + var command = new DeleteWorkflow { WorkflowId = Guid.NewGuid() }; + + Assert.Throws(() => GuardAppWorkflows.CanDelete(workflows, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_workflow_is_found() { - var command = new UpdateWorkflow { Workflow = Workflow.Default }; + var command = new DeleteWorkflow { WorkflowId = workflowId }; - GuardAppWorkflows.CanUpdate(command); + GuardAppWorkflows.CanDelete(workflows, command); } } }