Browse Source

Workflow flow.

pull/379/head
Sebastian Stehle 7 years ago
parent
commit
6c28b46437
  1. 1
      src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  2. 2
      src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  3. 4
      src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs
  4. 6
      src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  5. 15
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  6. 16
      src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureWorkflow.cs
  7. 71
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs
  8. 5
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  9. 18
      src/Squidex.Domain.Apps.Events/Apps/AppWorkflowConfigured.cs
  10. 6
      src/Squidex.Shared/Permissions.cs
  11. 86
      src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs
  12. 45
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs
  13. 61
      src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs
  14. 32
      src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowResponseDto.cs
  15. 32
      src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs
  16. 22
      src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs
  17. 6
      tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs
  18. 32
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
  19. 134
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs

1
src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -90,6 +90,7 @@ namespace Squidex.Domain.Apps.Core.Apps
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContents, app),
P.ForApp(P.AppPatterns, app),
P.ForApp(P.AppWorkflows, app),
P.ForApp(P.AppRules, app),
P.ForApp(P.AppSchemas, app));
}

2
src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -12,6 +12,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class Workflow
{
public static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>();
public static readonly Workflow Default = new Workflow(
new Dictionary<Status, WorkflowStep>
{

4
src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs

@ -12,13 +12,15 @@ namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class WorkflowStep
{
public static readonly Dictionary<Status, WorkflowTransition> EmptyTransitions = new Dictionary<Status, WorkflowTransition>();
public IReadOnlyDictionary<Status, WorkflowTransition> Transitions { get; }
public string Color { get; }
public bool NoUpdate { get; }
public WorkflowStep(IReadOnlyDictionary<Status, WorkflowTransition> transitions, string color, bool noUpdate = false)
public WorkflowStep(IReadOnlyDictionary<Status, WorkflowTransition> transitions, string color = null, bool noUpdate = false)
{
Guard.NotNull(transitions, nameof(transitions));

6
src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
@ -33,5 +34,10 @@ namespace Squidex.Domain.Apps.Core.Contents
return new Workflows(With(Guid.Empty, workflow));
}
public Workflow GetFirst()
{
return Values.FirstOrDefault() ?? Workflow.Default;
}
}
}

15
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -119,6 +119,16 @@ namespace Squidex.Domain.Apps.Entities.Apps
return Snapshot;
});
case ConfigureWorkflow configureWorkflow:
return UpdateReturn(configureWorkflow, c =>
{
GuardAppWorkflows.CanConfigure(c);
ConfigureWorkflow(c);
return Snapshot;
});
case AddLanguage addLanguage:
return UpdateReturn(addLanguage, c =>
{
@ -319,6 +329,11 @@ namespace Squidex.Domain.Apps.Entities.Apps
RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked()));
}
public void ConfigureWorkflow(ConfigureWorkflow command)
{
RaiseEvent(SimpleMapper.Map(command, new AppWorkflowConfigured()));
}
public void AddLanguage(AddLanguage command)
{
RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded()));

16
src/Squidex.Domain.Apps.Entities/Apps/Commands/ConfigureWorkflow.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class ConfigureWorkflow : AppCommand
{
public Workflow Workflow { get; set; }
}
}

71
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs

@ -0,0 +1,71 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public static class GuardAppWorkflows
{
public static void CanConfigure(ConfigureWorkflow command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot configure workflow.", e =>
{
if (command.Workflow == null)
{
e(Not.Defined("Workflow"), nameof(command.Workflow));
return;
}
var workflow = command.Workflow;
if (!workflow.Steps.ContainsKey(workflow.Initial))
{
e(Not.Defined("Initial step"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}");
}
var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}";
if (!workflow.Steps.ContainsKey(Status.Published))
{
e("Workflow must have a published step.", stepsPrefix);
}
foreach (var step in workflow.Steps)
{
var stepPrefix = $"{stepsPrefix}.{step.Key}";
if (step.Value == null)
{
e(Not.Defined("Step"), stepPrefix);
}
else
{
foreach (var transition in step.Value.Transitions)
{
var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{transition.Key}";
if (!workflow.Steps.ContainsKey(transition.Key))
{
e("Transition has an invalid target.", transitionPrefix);
}
if (transition.Value == null)
{
e(Not.Defined("Transition"), transitionPrefix);
}
}
}
}
});
}
}
}

5
src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -96,6 +96,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
Clients = Clients.Revoke(@event.Id);
}
protected void On(AppWorkflowConfigured @event)
{
Workflows = Workflows.Set(@event.Workflow);
}
protected void On(AppPatternAdded @event)
{
Patterns = Patterns.Add(@event.PatternId, @event.Name, @event.Pattern, @event.Message);

18
src/Squidex.Domain.Apps.Events/Apps/AppWorkflowConfigured.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppWorkflowConfigured))]
public sealed class AppWorkflowConfigured : AppEvent
{
public Workflow Workflow { get; set; }
}
}

6
src/Squidex.Shared/Permissions.cs

@ -81,6 +81,12 @@ namespace Squidex.Shared
public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update";
public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete";
public const string AppWorkflows = "squidex.apps.{app}.workflows";
public const string AppWorkflowsRead = "squidex.apps.{app}.workflows.read";
public const string AppWorkflowsCreate = "squidex.apps.{app}.workflows.create";
public const string AppWorkflowsUpdate = "squidex.apps.{app}.workflows.update";
public const string AppWorkflowsDelete = "squidex.apps.{app}.workflows.delete";
public const string AppBackups = "squidex.apps.{app}.backups";
public const string AppBackupsRead = "squidex.apps.{app}.backups.read";
public const string AppBackupsCreate = "squidex.apps.{app}.backups.create";

86
src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppWorkflowsController : ApiController
{
public AppWorkflowsController(ICommandBus commandBus)
: base(commandBus)
{
}
/// <summary>
/// Get app workflow.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => App workflows returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/workflow/")]
[ProducesResponseType(typeof(WorkflowResponseDto), 200)]
[ApiPermission(Permissions.AppWorkflowsRead)]
[ApiCosts(0)]
public IActionResult GetWorkflow(string app)
{
var response = WorkflowResponseDto.FromApp(App, this);
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
return Ok(response);
}
/// <summary>
/// Configure workflow of the app.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The new workflow.</param>
/// <returns>
/// 200 => Workflow configured.
/// 400 => Workflow is not valid.
/// 404 => App not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/workflow/")]
[ProducesResponseType(typeof(WorkflowResponseDto), 200)]
[ApiPermission(Permissions.AppWorkflowsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutWorkflow(string app, [FromBody] UpsertWorkflowDto request)
{
var command = request.ToCommand();
var response = await InvokeCommandAsync(command);
return Ok(response);
}
private async Task<WorkflowResponseDto> InvokeCommandAsync(ICommand command)
{
var context = await CommandBus.PublishAsync(command);
var result = context.Result<IAppEntity>();
var response = WorkflowResponseDto.FromApp(result, this);
return response;
}
}
}

45
src/Squidex/Areas/Api/Controllers/Apps/Models/UpsertWorkflowDto.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps.Commands;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class UpsertWorkflowDto
{
/// <summary>
/// The workflow steps.
/// </summary>
[Required]
public Dictionary<Status, WorkflowStepDto> Steps { get; set; }
/// <summary>
/// The initial step.
/// </summary>
public Status Initial { get; set; }
public ConfigureWorkflow ToCommand()
{
var workflow = new Workflow(
Steps?.ToDictionary(
x => x.Key,
x => new WorkflowStep(
x.Value?.Transitions.ToDictionary(
y => x.Key,
y => new WorkflowTransition(y.Value.Expression, y.Value.Role)) ?? WorkflowStep.EmptyTransitions,
x.Value.Color,
x.Value.NoUpdate)) ?? Workflow.EmptySteps,
Initial);
return new ConfigureWorkflow { Workflow = workflow };
}
}
}

61
src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowDto.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class WorkflowDto : Resource
{
/// <summary>
/// The workflow steps.
/// </summary>
[Required]
public Dictionary<Status, WorkflowStepDto> Steps { get; set; }
/// <summary>
/// The initial step.
/// </summary>
public Status Initial { get; set; }
public static WorkflowDto FromWorkflow(Workflow workflow, ApiController controller, string app)
{
var result = new WorkflowDto
{
Steps = workflow.Steps.ToDictionary(
x => x.Key,
x => SimpleMapper.Map(x.Value, new WorkflowStepDto
{
Transitions = x.Value.Transitions.ToDictionary(
y => y.Key,
y => new WorkflowTransitionDto { Expression = y.Value.Expression, Role = y.Value.Role })
})),
Initial = workflow.Initial
};
return result.CreateLinks(controller, app);
}
private WorkflowDto CreateLinks(ApiController controller, string app)
{
var values = new { app };
if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app))
{
AddPutLink("update", controller.Url<AppWorkflowsController>(x => nameof(x.PutWorkflow), values));
}
return this;
}
}
}

32
src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowResponseDto.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class WorkflowResponseDto : Resource
{
/// <summary>
/// The workflow.
/// </summary>
[Required]
public WorkflowDto Workflow { get; set; }
public static WorkflowResponseDto FromApp(IAppEntity app, ApiController controller)
{
var result = new WorkflowResponseDto
{
Workflow = WorkflowDto.FromWorkflow(app.Workflows.GetFirst(), controller, app.Name)
};
return result;
}
}
}

32
src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class WorkflowStepDto
{
/// <summary>
/// The transitions.
/// </summary>
[Required]
public Dictionary<Status, WorkflowTransitionDto> Transitions { get; set; }
/// <summary>
/// The optional color.
/// </summary>
public string Color { get; set; }
/// <summary>
/// Indicates if updates should not be allowed.
/// </summary>
public bool NoUpdate { get; set; }
}
}

22
src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class WorkflowTransitionDto
{
/// <summary>
/// The optional expression.
/// </summary>
public string Expression { get; set; }
/// <summary>
/// The optional restricted role.
/// </summary>
public string Role { get; set; }
}
}

6
tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs

@ -27,12 +27,10 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
StatusColors.Draft),
[Status.Archived] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>(),
StatusColors.Archived, true),
WorkflowStep.EmptyTransitions),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>(),
StatusColors.Archived)
WorkflowStep.EmptyTransitions)
}, Status.Draft);
[Fact]

32
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations;
@ -256,6 +257,27 @@ namespace Squidex.Domain.Apps.Entities.Apps
);
}
[Fact]
public async Task UpdateClient_should_create_events_and_update_state()
{
var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer };
await ExecuteCreateAsync();
await ExecuteAttachClientAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }),
CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer })
);
}
[Fact]
public async Task RevokeClient_should_create_events_and_update_state()
{
@ -277,23 +299,21 @@ namespace Squidex.Domain.Apps.Entities.Apps
}
[Fact]
public async Task UpdateClient_should_create_events_and_update_state()
public async Task ConfigureWorkflow_should_create_events_and_update_state()
{
var command = new UpdateClient { Id = clientId, Name = clientNewName, Role = Role.Developer };
var command = new ConfigureWorkflow { Workflow = Workflow.Default };
await ExecuteCreateAsync();
await ExecuteAttachClientAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name);
Assert.NotEmpty(sut.Snapshot.Workflows);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }),
CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer })
CreateEvent(new AppWorkflowConfigured { Workflow = Workflow.Default })
);
}

134
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs

@ -0,0 +1,134 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public class GuardAppWorkflowTests
{
[Fact]
public void CanConfigure_should_throw_exception_if_workflow_is_not_defined()
{
var command = new ConfigureWorkflow();
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command),
new ValidationError("Workflow is required.", "Workflow"));
}
[Fact]
public void CanConfigure_should_throw_exception_if_workflow_has_no_initial_step()
{
var command = new ConfigureWorkflow
{
Workflow = new Workflow(
new Dictionary<Status, WorkflowStep>
{
[Status.Published] = new WorkflowStep(WorkflowStep.EmptyTransitions)
},
default)
};
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command),
new ValidationError("Initial step is required.", "Workflow.Initial"));
}
[Fact]
public void CanConfigure_should_throw_exception_if_workflow_does_not_have_published_state()
{
var command = new ConfigureWorkflow
{
Workflow = new Workflow(
new Dictionary<Status, WorkflowStep>
{
[Status.Draft] = new WorkflowStep(WorkflowStep.EmptyTransitions)
},
Status.Draft)
};
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command),
new ValidationError("Workflow must have a published step.", "Workflow.Steps"));
}
[Fact]
public void CanConfigure_should_throw_exception_if_workflow_step_is_not_defined()
{
var command = new ConfigureWorkflow
{
Workflow = new Workflow(
new Dictionary<Status, WorkflowStep>
{
[Status.Published] = null
},
Status.Published)
};
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command),
new ValidationError("Step is required.", "Workflow.Steps.Published"));
}
[Fact]
public void CanConfigure_should_throw_exception_if_workflow_transition_is_invalid()
{
var command = new ConfigureWorkflow
{
Workflow = new Workflow(
new Dictionary<Status, WorkflowStep>
{
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = new WorkflowTransition()
})
},
Status.Published)
};
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command),
new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Draft"));
}
[Fact]
public void CanConfigure_should_throw_exception_if_workflow_transition_is_not_defined()
{
var command = new ConfigureWorkflow
{
Workflow = new Workflow(
new Dictionary<Status, WorkflowStep>
{
[Status.Draft] =
new WorkflowStep(
WorkflowStep.EmptyTransitions),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = null
})
},
Status.Published)
};
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command),
new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft"));
}
[Fact]
public void CanConfigure_should_not_throw_exception_if_workflow_is_valid()
{
var command = new ConfigureWorkflow { Workflow = Workflow.Default };
GuardAppWorkflows.CanConfigure(command);
}
}
}
Loading…
Cancel
Save