diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs index 6c4266e08..a56722c55 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Contents.Json { public IEnumerable SupportedTypes { - get { yield return typeof(Status2); } + get { yield return typeof(Status); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) @@ -31,12 +31,12 @@ namespace Squidex.Domain.Apps.Core.Contents.Json throw new JsonException($"Expected String, but got {reader.TokenType}."); } - return new Status2(reader.Value.ToString()); + return new Status(reader.Value.ToString()); } public override bool CanConvert(Type objectType) { - return objectType == typeof(Status2); + return objectType == typeof(Status); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs index c20e0c4eb..d4c0374c8 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -5,12 +5,57 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure; +using System; + namespace Squidex.Domain.Apps.Core.Contents { - public enum Status + public struct Status : IEquatable { - Draft, - Archived, - Published + public static readonly Status Archived = new Status("Archived"); + public static readonly Status Draft = new Status("Draft"); + public static readonly Status Published = new Status("Published"); + + private readonly string name; + + public string Name + { + get { return name ?? "Unknown"; } + } + + public Status(string name) + { + this.name = name; + } + + public override bool Equals(object obj) + { + return obj is Status status && Equals(status); + } + + public bool Equals(Status other) + { + return string.Equals(name, other.name); + } + + public override int GetHashCode() + { + return name?.GetHashCode() ?? 0; + } + + public override string ToString() + { + return name; + } + + public static bool operator ==(Status lhs, Status rhs) + { + return lhs.Equals(rhs); + } + + public static bool operator !=(Status lhs, Status rhs) + { + return !lhs.Equals(rhs); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs deleted file mode 100644 index 7ce6e830c..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; -using System; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public struct Status2 : IEquatable - { - public static readonly Status2 Published = new Status2("Published"); - - public string Name { get; } - - public Status2(string name) - { - Guard.NotNullOrEmpty(name, nameof(name)); - - Name = name; - } - - public override bool Equals(object obj) - { - return obj is Status2 status && Equals(status); - } - - public bool Equals(Status2 other) - { - return Name.Equals(other.Name); - } - - public override int GetHashCode() - { - return Name.GetHashCode(); - } - - public override string ToString() - { - return Name; - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs deleted file mode 100644 index 9e3900deb..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Domain.Apps.Core.Contents -{ - public enum StatusChange - { - Archived, - Published, - Restored, - Unpublished - } -} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs deleted file mode 100644 index 7add93c24..000000000 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Domain.Apps.Core.Contents -{ - public static class StatusFlow - { - private static readonly Dictionary Flow = new Dictionary - { - [Status.Draft] = new[] { Status.Published, Status.Archived }, - [Status.Archived] = new[] { Status.Draft }, - [Status.Published] = new[] { Status.Draft, Status.Archived } - }; - - public static bool Exists(Status status) - { - return Flow.ContainsKey(status); - } - - public static bool CanChange(Status status, Status toStatus) - { - return Flow.TryGetValue(status, out var state) && state.Contains(toStatus); - } - - public static IEnumerable Next(Status status) - { - return Flow.TryGetValue(status, out var result) ? result : Enumerable.Empty(); - } - } -} diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs index 45148a8e2..7ad49332d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs @@ -9,12 +9,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents { public enum EnrichedContentEventType { - Archived, Created, Deleted, - Published, - Restored, - Unpublished, + StatusChanged, Updated } } diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs index 9c8f3eba7..fe2e0649c 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs @@ -51,7 +51,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents [BsonRequired] [BsonElement("ss")] - [BsonRepresentation(BsonType.String)] public Status Status { get; set; } [BsonIgnoreIfNull] diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs index 0b9bf91f0..5d59c836a 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs @@ -12,7 +12,7 @@ using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public sealed class StatusSerializer : SerializerBase + public sealed class StatusSerializer : SerializerBase { private static volatile int isRegistered; @@ -24,14 +24,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public override Status2 Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + public override Status Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { var value = context.Reader.ReadString(); - return new Status2(value); + return new Status(value); } - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status2 value) + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status value) { context.Writer.WriteString(value.Name); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index dff657548..0a155f703 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -8,7 +8,6 @@ using System; using System.Threading.Tasks; using Orleans; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -60,23 +59,8 @@ namespace Squidex.Domain.Apps.Entities.Contents case ContentUpdated _: result.Type = EnrichedContentEventType.Updated; break; - case ContentStatusChanged contentStatusChanged: - switch (contentStatusChanged.Change) - { - case StatusChange.Published: - result.Type = EnrichedContentEventType.Published; - break; - case StatusChange.Unpublished: - result.Type = EnrichedContentEventType.Unpublished; - break; - case StatusChange.Archived: - result.Type = EnrichedContentEventType.Archived; - break; - case StatusChange.Restored: - result.Type = EnrichedContentEventType.Restored; - break; - } - + case ContentStatusChanged _: + result.Type = EnrichedContentEventType.StatusChanged; break; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 9c5a5f3f7..8b1b6aac1 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -8,9 +8,7 @@ using System; using NodaTime; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Contents { @@ -41,24 +39,5 @@ namespace Squidex.Domain.Apps.Entities.Contents public Status Status { get; set; } public bool IsPending { get; set; } - - public static ContentEntity Create(CreateContent command, EntityCreatedResult result) - { - var now = SystemClock.Instance.GetCurrentInstant(); - - var response = new ContentEntity - { - Id = command.ContentId, - Data = result.IdOrValue, - Version = result.Version, - Created = now, - CreatedBy = command.Actor, - LastModified = now, - LastModifiedBy = command.Actor, - Status = command.Publish ? Status.Published : Status.Draft - }; - - return response; - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index baae5d2dd..0f73ab461 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -32,6 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly IAssetRepository assetRepository; private readonly IContentRepository contentRepository; private readonly IScriptEngine scriptEngine; + private readonly IContentWorkflow contentWorkflow; public ContentGrain( IStore store, @@ -39,17 +40,20 @@ namespace Squidex.Domain.Apps.Entities.Contents IAppProvider appProvider, IAssetRepository assetRepository, IScriptEngine scriptEngine, + IContentWorkflow contentWorkflow, IContentRepository contentRepository) : base(store, log) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); Guard.NotNull(contentRepository, nameof(contentRepository)); this.appProvider = appProvider; this.scriptEngine = scriptEngine; this.assetRepository = assetRepository; + this.contentWorkflow = contentWorkflow; this.contentRepository = contentRepository; } @@ -79,25 +83,27 @@ namespace Squidex.Domain.Apps.Entities.Contents await ctx.ExecuteScriptAsync(s => s.Change, "Published", c, c.Data); } - Create(c); + var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema); + + Create(c, status); return Snapshot; }); case UpdateContent updateContent: - return UpdateReturnAsync(updateContent, c => + return UpdateReturnAsync(updateContent, async c => { - GuardContent.CanUpdate(c); + await GuardContent.CanUpdate(Snapshot, contentWorkflow, c); - return UpdateAsync(c, x => c.Data, false); + return await UpdateAsync(c, x => c.Data, false); }); case PatchContent patchContent: - return UpdateReturnAsync(patchContent, c => + return UpdateReturnAsync(patchContent, async c => { - GuardContent.CanPatch(c); + await GuardContent.CanPatch(Snapshot, contentWorkflow, c); - return UpdateAsync(c, c.Data.MergeInto, true); + return await UpdateAsync(c, c.Data.MergeInto, true); }); case ChangeContentStatus changeContentStatus: @@ -107,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, Snapshot.Id, () => "Failed to change content."); - GuardContent.CanChangeContentStatus(ctx.Schema, Snapshot.IsPending, Snapshot.Status, c); + await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c); if (c.DueTime.HasValue) { @@ -121,28 +127,11 @@ namespace Squidex.Domain.Apps.Entities.Contents } else { - StatusChange reason; - - if (c.Status == Status.Published) - { - reason = StatusChange.Published; - } - else if (c.Status == Status.Archived) - { - reason = StatusChange.Archived; - } - else if (Snapshot.Status == Status.Published) - { - reason = StatusChange.Unpublished; - } - else - { - reason = StatusChange.Restored; - } - - await ctx.ExecuteScriptAsync(s => s.Change, reason, c, Snapshot.Data); - - ChangeStatus(c, reason); + var operation = c.Status == Status.Published ? "Published" : "StatusChanged"; + + await ctx.ExecuteScriptAsync(s => s.Change, operation, c, Snapshot.Data); + + ChangeStatus(c); } } } @@ -227,13 +216,13 @@ namespace Squidex.Domain.Apps.Entities.Contents return Snapshot; } - public void Create(CreateContent command) + public void Create(CreateContent command, Status status) { - RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); + RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status })); if (command.Publish) { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published })); + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published })); } } @@ -272,9 +261,9 @@ namespace Squidex.Domain.Apps.Entities.Contents RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value })); } - public void ChangeStatus(ChangeContentStatus command, StatusChange reason) + public void ChangeStatus(ChangeContentStatus command) { - RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = reason })); + RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); } private void RaiseEvent(SchemaEvent @event) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs index a75dd30a2..b1928480a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; @@ -16,25 +17,23 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class DefaultContentWorkflow : IContentWorkflow { - private static readonly Status2 Draft = new Status2("Draft"); - private static readonly Status2 Archived = new Status2("Archived"); - private static readonly Status2 Published = new Status2("Published"); + private static readonly Status[] All = { Status.Archived, Status.Draft, Status.Published }; - private static readonly Dictionary Flow = new Dictionary + private static readonly Dictionary Flow = new Dictionary { - [Draft] = new[] { Published, Archived }, - [Archived] = new[] { Draft }, - [Published] = new[] { Draft, Archived } + [Status.Draft] = new[] { Status.Archived, Status.Published }, + [Status.Archived] = new[] { Status.Draft }, + [Status.Published] = new[] { Status.Draft, Status.Archived } }; - public Task GetInitialStatusAsync(ISchemaEntity schema) + public Task GetInitialStatusAsync(ISchemaEntity schema) { - return Task.FromResult(Draft); + return Task.FromResult(Status.Draft); } - public Task IsValidNextStatus(IContentEntity content, Status2 next) + public Task IsValidNextStatus(IContentEntity content, Status next) { - return TaskHelper.True; + return Task.FromResult(Flow.TryGetValue(content.Status, out var state) && state.Contains(next)); } public Task CanUpdateAsync(IContentEntity content) @@ -42,34 +41,14 @@ namespace Squidex.Domain.Apps.Entities.Contents return TaskHelper.True; } - public Task GetNextsAsync(IContentEntity content) + public Task GetNextsAsync(IContentEntity content) { - Status2 statusToCheck; - - switch (content.Status) - { - case Status.Draft: - statusToCheck = Draft; - break; - case Status.Archived: - statusToCheck = Archived; - break; - case Status.Published: - statusToCheck = Published; - break; - default: - { - statusToCheck = Draft; - break; - } - } - - return Task.FromResult(Flow.TryGetValue(statusToCheck, out var result) ? result : Array.Empty()); + return Task.FromResult(Flow.TryGetValue(content.Status, out var result) ? result : Array.Empty()); } - public Task GetAllAsync(ISchemaEntity schema) + public Task GetAllAsync(ISchemaEntity schema) { - return Task.FromResult(new[] { Draft, Archived, Published } ); + return Task.FromResult(All); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs index 5e1b6c973..8f3207ebf 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs @@ -28,8 +28,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static readonly IGraphType Float = new FloatGraphType(); - public static readonly IGraphType Status = new EnumerationGraphType(); - public static readonly IGraphType String = new StringGraphType(); public static readonly IGraphType Boolean = new BooleanGraphType(); @@ -46,8 +44,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean); - public static readonly IGraphType NonNullStatusType = new NonNullGraphType(Status); - public static readonly IGraphType NoopDate = new NoopGraphType(Date); public static readonly IGraphType NoopJson = new NoopGraphType(Json); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index ec8878874..a6d876742 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -73,8 +73,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = "status", - ResolvedType = AllTypes.NonNullStatusType, - Resolver = Resolve(x => x.Status), + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), Description = $"The the status of the {schemaName} content." }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs index 379bd601f..35a3acca7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; @@ -30,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards } } - public static void CanUpdate(UpdateContent command) + public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command) { Guard.NotNull(command, nameof(command)); @@ -38,9 +39,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { ValidateData(command, e); }); + + await ValidateCanUpdate(content, contentWorkflow); } - public static void CanPatch(PatchContent command) + public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command) { Guard.NotNull(command, nameof(command)); @@ -48,6 +51,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards { ValidateData(command, e); }); + + await ValidateCanUpdate(content, contentWorkflow); } public static void CanDiscardChanges(bool isPending, DiscardChanges command) @@ -60,33 +65,29 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards } } - public static void CanChangeContentStatus(ISchemaEntity schema, bool isPending, Status status, ChangeContentStatus command) + public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command) { Guard.NotNull(command, nameof(command)); if (schema.SchemaDef.IsSingleton && command.Status != Status.Published) { - throw new DomainException("Singleton content archived or unpublished."); + throw new DomainException("Singleton content cannot be changed."); } - Validate.It(() => "Cannot change status.", e => + return Validate.It(() => "Cannot change status.", async e => { - if (!StatusFlow.Exists(command.Status)) + if (!await contentWorkflow.IsValidNextStatus(content, command.Status)) { - e(Not.Valid("Status"), nameof(command.Status)); - } - else if (!StatusFlow.CanChange(status, command.Status)) - { - if (status == command.Status && status == Status.Published) + if (content.Status == command.Status && content.Status == Status.Published) { - if (!isPending) + if (!content.IsPending) { e("Content has no changes to publish.", nameof(command.Status)); } } else { - e($"Cannot change status from {status} to {command.Status}.", nameof(command.Status)); + e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status)); } } @@ -114,5 +115,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards e(Not.Defined("Data"), nameof(command.Data)); } } + + private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow) + { + if (!await contentWorkflow.CanUpdateAsync(content)) + { + throw new DomainException($"The workflow does not allow updates at status {content.Status}"); + } + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index e2b21d0af..06de365be 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -13,14 +13,14 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentWorkflow { - Task GetInitialStatusAsync(ISchemaEntity schema); + Task GetInitialStatusAsync(ISchemaEntity schema); - Task IsValidNextStatus(IContentEntity content, Status2 next); + Task IsValidNextStatus(IContentEntity content, Status next); Task CanUpdateAsync(IContentEntity content); - Task GetNextsAsync(IContentEntity content); + Task GetNextsAsync(IContentEntity content); - Task GetAllAsync(ISchemaEntity schema); + Task GetAllAsync(ISchemaEntity schema); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 1f2169431..0f0234dc9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -50,6 +50,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.State SimpleMapper.Map(@event, this); UpdateData(null, @event.Data, false); + + if (Status == default) + { + Status = Status.Draft; + } } protected void On(ContentChangesPublished @event) diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs index ed06a71ec..317b3b176 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs @@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Events.Contents [EventType(nameof(ContentCreated))] public sealed class ContentCreated : ContentEvent { + public Status Status { get; set; } + public NamedContentData Data { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs index 81b95e5fb..2c97f5902 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs @@ -13,8 +13,6 @@ namespace Squidex.Domain.Apps.Events.Contents [EventType(nameof(ContentStatusChanged))] public sealed class ContentStatusChanged : ContentEvent { - public StatusChange? Change { get; set; } - public Status Status { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index a8a4d1e87..833c67dbe 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -373,7 +373,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { await contentQuery.GetSchemaOrThrowAsync(Context(), name); - if (!this.HasPermission(Helper.StatusPermission(app, name, Status2.Published))) + if (!this.HasPermission(Helper.StatusPermission(app, name, Status.Published))) { return new ForbidResult(); } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs index be1f7ef46..8644c925a 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Helper.cs @@ -14,13 +14,6 @@ namespace Squidex.Areas.Api.Controllers.Contents public static class Helper { public static Permission StatusPermission(string app, string schema, Status status) - { - var id = Permissions.AppContentsStatus.Replace("{status}", status.ToString()); - - return Permissions.ForApp(id, app, schema); - } - - public static Permission StatusPermission(string app, string schema, Status2 status) { var id = Permissions.AppContentsStatus.Replace("{status}", status.Name); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index c699d7017..c83638142 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -131,7 +131,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddPutLink("draft/discard", controller.Url(x => nameof(x.DiscardDraft), values)); } - if (controller.HasPermission(Helper.StatusPermission(app, schema, Status2.Published))) + if (controller.HasPermission(Helper.StatusPermission(app, schema, Status.Published))) { AddPutLink("draft/publish", controller.Url(x => nameof(x.PutContentStatus), values)); } @@ -146,7 +146,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models if (Status == Status.Published) { - AddPutLink("draft/propose", controller.Url(x => nameof(x.PutContent), values) + "?asDraft=true"); + AddPutLink("draft/propose", controller.Url((ContentsController x) => nameof(x.PutContent), values) + "?asDraft=true"); } AddPatchLink("patch", controller.Url(x => nameof(x.PatchContent), values)); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 928cc12b2..afcecb7fe 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -35,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// The possible statuses. /// [Required] - public Status2[] Statuses { get; set; } + public Status[] Statuses { get; set; } public string ToEtag() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs deleted file mode 100644 index 30126894c..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs +++ /dev/null @@ -1,34 +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 Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Contents -{ - public class StatusFlowTests - { - [Fact] - public void Should_make_tests() - { - Assert.True(StatusFlow.Exists(Status.Draft)); - Assert.True(StatusFlow.Exists(Status.Archived)); - Assert.True(StatusFlow.Exists(Status.Published)); - - Assert.True(StatusFlow.CanChange(Status.Draft, Status.Archived)); - Assert.True(StatusFlow.CanChange(Status.Draft, Status.Published)); - - Assert.True(StatusFlow.CanChange(Status.Published, Status.Draft)); - Assert.True(StatusFlow.CanChange(Status.Published, Status.Archived)); - - Assert.True(StatusFlow.CanChange(Status.Archived, Status.Draft)); - - Assert.False(StatusFlow.Exists((Status)int.MaxValue)); - Assert.False(StatusFlow.CanChange(Status.Archived, Status.Published)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs index 5abdd5f2d..138e399c1 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs @@ -15,16 +15,34 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_initialize_status_from_string() { - var result = new Status2("Draft"); + var result = new Status("Custom"); + + Assert.Equal("Custom", result.Name); + Assert.Equal("Custom", result.ToString()); + } + + [Fact] + public void Should_provide_draft_status() + { + var result = Status.Draft; Assert.Equal("Draft", result.Name); Assert.Equal("Draft", result.ToString()); } + [Fact] + public void Should_provide_archived_status() + { + var result = Status.Archived; + + Assert.Equal("Archived", result.Name); + Assert.Equal("Archived", result.ToString()); + } + [Fact] public void Should_provide_published_status() { - var result = Status2.Published; + var result = Status.Published; Assert.Equal("Published", result.Name); Assert.Equal("Published", result.ToString()); @@ -33,10 +51,10 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] public void Should_make_correct_equal_comparisons() { - var status_1_a = new Status2("Draft"); - var status_1_b = new Status2("Draft"); + var status_1_a = Status.Draft; + var status_1_b = Status.Draft; - var status2_a = new Status2("Published"); + var status2_a = Status.Published; Assert.Equal(status_1_a, status_1_b); Assert.Equal(status_1_a.GetHashCode(), status_1_b.GetHashCode()); @@ -45,12 +63,18 @@ namespace Squidex.Domain.Apps.Core.Model.Contents Assert.NotEqual(status_1_a, status2_a); Assert.NotEqual(status_1_a.GetHashCode(), status2_a.GetHashCode()); Assert.False(status_1_a.Equals((object)status2_a)); + + Assert.True(status_1_a == status_1_b); + Assert.True(status_1_a != status2_a); + + Assert.False(status_1_a != status_1_b); + Assert.False(status_1_a == status2_a); } [Fact] public void Should_serialize_and_deserialize() { - var status = new Status2("Draft"); + var status = Status.Draft; var serialized = status.SerializeAndDeserialize(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index 03b547993..1e49dc457 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -11,7 +11,6 @@ using System.Collections.ObjectModel; using System.Threading.Tasks; using FakeItEasy; using Orleans; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.Triggers; @@ -54,10 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents new object[] { new ContentCreated(), EnrichedContentEventType.Created }, new object[] { new ContentUpdated(), EnrichedContentEventType.Updated }, new object[] { new ContentDeleted(), EnrichedContentEventType.Deleted }, - new object[] { new ContentStatusChanged { Change = StatusChange.Archived }, EnrichedContentEventType.Archived }, - new object[] { new ContentStatusChanged { Change = StatusChange.Published }, EnrichedContentEventType.Published }, - new object[] { new ContentStatusChanged { Change = StatusChange.Restored }, EnrichedContentEventType.Restored }, - new object[] { new ContentStatusChanged { Change = StatusChange.Unpublished }, EnrichedContentEventType.Unpublished } + new object[] { new ContentStatusChanged(), EnrichedContentEventType.StatusChanged } }; [Theory] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs index f5a96de1b..8edba81dd 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs @@ -33,6 +33,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { private readonly ISchemaEntity schema = A.Fake(); private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IContentRepository contentRepository = A.Dummy(); + private readonly IContentWorkflow contentWorkflow = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly IAppEntity app = A.Fake(); private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); @@ -100,9 +102,15 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) .ReturnsLazily(x => x.GetArgument(0).Data); + A.CallTo(() => contentWorkflow.CanUpdateAsync(A.Ignored)) + .Returns(true); + + A.CallTo(() => contentWorkflow.IsValidNextStatus(A.Ignored, A.Ignored)) + .Returns(true); + patched = patch.MergeInto(data); - sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, A.Dummy()); + sut = new ContentGrain(Store, A.Dummy(), appProvider, A.Dummy(), scriptEngine, contentWorkflow, contentRepository); sut.ActivateAsync(Id).Wait(); } @@ -135,6 +143,26 @@ namespace Squidex.Domain.Apps.Entities.Contents .MustNotHaveHappened(); } + [Fact] + public async Task Create_should_create_events_and_update_state_with_custom_initial_status() + { + var command = new CreateContent { Data = data }; + + A.CallTo(() => contentWorkflow.GetInitialStatusAsync(schema)) + .Returns(Status.Archived); + + var result = await sut.ExecuteAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(Status.Archived, sut.Snapshot.Status); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentCreated { Data = data, Status = Status.Archived }) + ); + } + [Fact] public async Task Create_should_also_publish() { @@ -147,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( CreateContentEvent(new ContentCreated { Data = data }), - CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) ); A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")) @@ -315,7 +343,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) ); A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) @@ -337,7 +365,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Archived, Change = StatusChange.Archived }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Archived }) ); A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) @@ -360,7 +388,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Unpublished }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Draft }) ); A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) @@ -383,7 +411,7 @@ namespace Squidex.Domain.Apps.Entities.Contents LastEvents .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Draft, Change = StatusChange.Restored }) + CreateContentEvent(new ContentStatusChanged { Status = Status.Draft }) ); A.CallTo(() => scriptEngine.Execute(A.Ignored, "")) @@ -448,6 +476,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var command = new ChangeContentStatus { Status = Status.Draft, JobId = sut.Snapshot.ScheduleJob.Id }; + A.CallTo(() => contentWorkflow.IsValidNextStatus(sut.Snapshot, command.Status)) + .Returns(false); + var result = await sut.ExecuteAsync(CreateContentCommand(command)); result.ShouldBeEquivalent(sut.Snapshot); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index 546454b83..e6fb4334b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -520,7 +520,12 @@ namespace Squidex.Domain.Apps.Entities.Contents .Returns((ISchemaEntity)null); } - private IContentEntity CreateContent(Guid id, Status status = Status.Published) + private IContentEntity CreateContent(Guid id) + { + return CreateContent(id, Status.Published); + } + + private IContentEntity CreateContent(Guid id, Status status) { var content = A.Fake(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index 25d9d8bb8..6a1b5f32a 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -21,15 +21,15 @@ namespace Squidex.Domain.Apps.Entities.Contents { var result = await sut.GetInitialStatusAsync(null); - Assert.Equal(new Status2("Draft"), result); + Assert.Equal(Status.Draft, result); } [Fact] public async Task Should_check_is_valid_next() { - var entity = CreateMockContentEntity(Status.Draft); + var entity = CreateContent(Status.Published); - var result = await sut.IsValidNextStatus(entity, new Status2("Draft")); + var result = await sut.IsValidNextStatus(entity, Status.Draft); Assert.True(result); } @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_always_be_able_to_update() { - var entity = CreateMockContentEntity(Status.Draft); + var entity = CreateContent(Status.Published); var result = await sut.CanUpdateAsync(entity); @@ -47,9 +47,9 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_get_next_statuses_for_draft() { - var content = CreateMockContentEntity(Status.Draft); + var content = CreateContent(Status.Draft); - var expected = new[] { new Status2("Published"), new Status2("Archived") }; + var expected = new[] { Status.Archived, Status.Published }; var result = await sut.GetNextsAsync(content); @@ -59,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_get_next_statuses_for_archived() { - var content = CreateMockContentEntity(Status.Archived); + var content = CreateContent(Status.Archived); - var expected = new[] { new Status2("Draft") }; + var expected = new[] { Status.Draft }; var result = await sut.GetNextsAsync(content); @@ -71,9 +71,9 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_get_next_statuses_for_published() { - var content = CreateMockContentEntity(Status.Published); + var content = CreateContent(Status.Published); - var expected = new[] { new Status2("Draft"), new Status2("Archived") }; + var expected = new[] { Status.Draft, Status.Archived }; var result = await sut.GetNextsAsync(content); @@ -83,14 +83,14 @@ namespace Squidex.Domain.Apps.Entities.Contents [Fact] public async Task Should_return_all_statuses() { - var expected = new[] { new Status2("Draft"), new Status2("Archived"), new Status2("Published") }; + var expected = new[] { Status.Archived, Status.Draft, Status.Published }; var result = await sut.GetAllAsync(null); Assert.Equal(expected, result); } - private IContentEntity CreateMockContentEntity(Status status) + private IContentEntity CreateContent(Status status) { var content = A.Fake(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index d3f6b5ea1..cfc9e817b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -159,7 +159,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL LastModified = now, LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), Data = data, - DataDraft = dataDraft + DataDraft = dataDraft, + Status = Status.Draft }; return content; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs index 5dda8794d..1f1eb91cb 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading.Tasks; using FakeItEasy; using NodaTime; using Squidex.Domain.Apps.Core.Contents; @@ -21,6 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard public class GuardContentTests { private readonly ISchemaEntity schema = A.Fake(); + private readonly IContentWorkflow contentWorkflow = A.Fake(); private readonly Instant dueTimeInPast = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(1)); [Fact] @@ -65,119 +67,155 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard } [Fact] - public void CanUpdate_should_throw_exception_if_data_is_null() + public async Task CanUpdate_should_throw_exception_if_data_is_null() { SetupSingleton(false); + SetupCanUpdate(true); + var content = CreateContent(Status.Draft, false); var command = new UpdateContent(); - ValidationAssert.Throws(() => GuardContent.CanUpdate(command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanUpdate(content, contentWorkflow, command), new ValidationError("Data is required.", "Data")); } [Fact] - public void CanUpdate_should_not_throw_exception_if_data_is_not_null() + public async Task CanUpdate_should_throw_exception_if_workflow_blocks_it() { SetupSingleton(false); + SetupCanUpdate(false); + var content = CreateContent(Status.Draft, false); var command = new UpdateContent { Data = new NamedContentData() }; - GuardContent.CanUpdate(command); + await Assert.ThrowsAsync(() => GuardContent.CanUpdate(content, contentWorkflow, command)); } [Fact] - public void CanPatch_should_throw_exception_if_data_is_null() + public async Task CanUpdate_should_not_throw_exception_if_data_is_not_null() { SetupSingleton(false); + SetupCanUpdate(true); + var content = CreateContent(Status.Draft, false); + var command = new UpdateContent { Data = new NamedContentData() }; + + await GuardContent.CanUpdate(content, contentWorkflow, command); + } + + [Fact] + public async Task CanPatch_should_throw_exception_if_data_is_null() + { + SetupSingleton(false); + SetupCanUpdate(true); + + var content = CreateContent(Status.Draft, false); var command = new PatchContent(); - ValidationAssert.Throws(() => GuardContent.CanPatch(command), + await ValidationAssert.ThrowsAsync(() => GuardContent.CanPatch(content, contentWorkflow, command), new ValidationError("Data is required.", "Data")); } [Fact] - public void CanPatch_should_not_throw_exception_if_data_is_not_null() + public async Task CanPatch_should_throw_exception_if_workflow_blocks_it() { SetupSingleton(false); + SetupCanUpdate(false); + var content = CreateContent(Status.Draft, false); var command = new PatchContent { Data = new NamedContentData() }; - GuardContent.CanPatch(command); + await Assert.ThrowsAsync(() => GuardContent.CanPatch(content, contentWorkflow, command)); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_status_not_valid() + public async Task CanPatch_should_not_throw_exception_if_data_is_not_null() { SetupSingleton(false); + SetupCanUpdate(true); - var command = new ChangeContentStatus { Status = (Status)10 }; + var content = CreateContent(Status.Draft, false); + var command = new PatchContent { Data = new NamedContentData() }; - ValidationAssert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Archived, command), - new ValidationError("Status is not a valid value.", "Status")); + await GuardContent.CanPatch(content, contentWorkflow, command); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_status_flow_not_valid() + public async Task CanChangeStatus_should_throw_exception_if_publishing_without_pending_changes() { SetupSingleton(false); + var content = CreateContent(Status.Published, false); var command = new ChangeContentStatus { Status = Status.Published }; - ValidationAssert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Archived, command), - new ValidationError("Cannot change status from Archived to Published.", "Status")); + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + new ValidationError("Content has no changes to publish.", "Status")); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_due_date_in_past() + public async Task CanChangeStatus_should_throw_exception_if_singleton() { - SetupSingleton(false); + SetupSingleton(true); - var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast }; + var content = CreateContent(Status.Published, false); + var command = new ChangeContentStatus { Status = Status.Draft }; - ValidationAssert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Draft, command), - new ValidationError("Due time must be in the future.", "DueTime")); + await Assert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command)); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_publishing_without_pending_changes() + public async Task CanChangeStatus_should_not_throw_exception_if_publishing_with_pending_changes() { - SetupSingleton(false); + SetupSingleton(true); + var content = CreateContent(Status.Published, true); var command = new ChangeContentStatus { Status = Status.Published }; - ValidationAssert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Published, command), - new ValidationError("Content has no changes to publish.", "Status")); + await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command); } [Fact] - public void CanChangeContentStatus_should_throw_exception_if_singleton() + public async Task CanChangeStatus_should_throw_exception_if_due_date_in_past() { - SetupSingleton(true); + SetupSingleton(false); - var command = new ChangeContentStatus { Status = Status.Draft }; + var content = CreateContent(Status.Draft, false); + var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTimeInPast }; + + A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + .Returns(true); - Assert.Throws(() => GuardContent.CanChangeContentStatus(schema, false, Status.Published, command)); + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + new ValidationError("Due time must be in the future.", "DueTime")); } [Fact] - public void CanChangeContentStatus_should_not_throw_exception_if_publishing_with_pending_changes() + public async Task CanChangeStatus_should_throw_exception_if_status_flow_not_valid() { - SetupSingleton(true); + SetupSingleton(false); + var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published }; - GuardContent.CanChangeContentStatus(schema, true, Status.Published, command); + A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + .Returns(false); + + await ValidationAssert.ThrowsAsync(() => GuardContent.CanChangeStatus(schema, content, contentWorkflow, command), + new ValidationError("Cannot change status from Draft to Published.", "Status")); } [Fact] - public void CanChangeContentStatus_should_not_throw_exception_if_status_flow_valid() + public async Task CanChangeStatus_should_not_throw_exception_if_status_flow_valid() { SetupSingleton(false); + var content = CreateContent(Status.Draft, false); var command = new ChangeContentStatus { Status = Status.Published }; - GuardContent.CanChangeContentStatus(schema, false, Status.Draft, command); + A.CallTo(() => contentWorkflow.IsValidNextStatus(content, command.Status)) + .Returns(true); + + await GuardContent.CanChangeStatus(schema, content, contentWorkflow, command); } [Fact] @@ -218,10 +256,26 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard GuardContent.CanDelete(schema, command); } + private void SetupCanUpdate(bool canUpdate) + { + A.CallTo(() => contentWorkflow.CanUpdateAsync(A.Ignored)) + .Returns(canUpdate); + } + private void SetupSingleton(bool isSingleton) { A.CallTo(() => schema.SchemaDef) .Returns(new Schema("schema", isSingleton: isSingleton)); } + + private IContentEntity CreateContent(Status status, bool isPending) + { + var content = A.Fake(); + + A.CallTo(() => content.Status).Returns(status); + A.CallTo(() => content.IsPending).Returns(isPending); + + return content; + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs index ce50fc208..40556eb16 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { private sealed class TestObject { - public Status2 Status { get; set; } + public Status Status { get; set; } } [Fact] @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb var source = new TestObject { - Status = new Status2("Published") + Status = Status.Published }; var document = new BsonDocument();