Browse Source

All tests green now.

pull/373/head
Sebastian Stehle 7 years ago
parent
commit
9fed02581b
  1. 6
      src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs
  2. 53
      src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  3. 46
      src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs
  4. 17
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
  5. 37
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs
  6. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs
  7. 1
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  8. 8
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs
  9. 20
      src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  10. 21
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  11. 59
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  12. 49
      src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  13. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  14. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  15. 35
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  16. 8
      src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  17. 5
      src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs
  18. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs
  19. 2
      src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs
  20. 2
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  21. 7
      src/Squidex/Areas/Api/Controllers/Contents/Helper.cs
  22. 4
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  23. 2
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
  24. 34
      tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs
  25. 36
      tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusTests.cs
  26. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  27. 43
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs
  28. 7
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  29. 24
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs
  30. 3
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  31. 120
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs
  32. 4
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/StatusSerializerTests.cs

6
src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs

@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
{
public IEnumerable<Type> 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);
}
}
}

53
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<Status>
{
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);
}
}
}

46
src/Squidex.Domain.Apps.Core.Model/Contents/Status2.cs

@ -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<Status2>
{
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;
}
}
}

17
src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs

@ -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
}
}

37
src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs

@ -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<Status, Status[]> Flow = new Dictionary<Status, Status[]>
{
[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<Status> Next(Status status)
{
return Flow.TryGetValue(status, out var result) ? result : Enumerable.Empty<Status>();
}
}
}

5
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
}
}

1
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]

8
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<Status2>
public sealed class StatusSerializer : SerializerBase<Status>
{
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);
}

20
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;
}

21
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<NamedContentData> 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;
}
}
}

59
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<Guid> 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)

49
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<Status2, Status2[]> Flow = new Dictionary<Status2, Status2[]>
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
{
[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<Status2> GetInitialStatusAsync(ISchemaEntity schema)
public Task<Status> GetInitialStatusAsync(ISchemaEntity schema)
{
return Task.FromResult(Draft);
return Task.FromResult(Status.Draft);
}
public Task<bool> IsValidNextStatus(IContentEntity content, Status2 next)
public Task<bool> IsValidNextStatus(IContentEntity content, Status next)
{
return TaskHelper.True;
return Task.FromResult(Flow.TryGetValue(content.Status, out var state) && state.Contains(next));
}
public Task<bool> CanUpdateAsync(IContentEntity content)
@ -42,34 +41,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
return TaskHelper.True;
}
public Task<Status2[]> GetNextsAsync(IContentEntity content)
public Task<Status[]> 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<Status2>());
return Task.FromResult(Flow.TryGetValue(content.Status, out var result) ? result : Array.Empty<Status>());
}
public Task<Status2[]> GetAllAsync(ISchemaEntity schema)
public Task<Status[]> GetAllAsync(ISchemaEntity schema)
{
return Task.FromResult(new[] { Draft, Archived, Published } );
return Task.FromResult(All);
}
}
}

4
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<Status>();
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);

4
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."
});

35
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}");
}
}
}
}

8
src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs

@ -13,14 +13,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentWorkflow
{
Task<Status2> GetInitialStatusAsync(ISchemaEntity schema);
Task<Status> GetInitialStatusAsync(ISchemaEntity schema);
Task<bool> IsValidNextStatus(IContentEntity content, Status2 next);
Task<bool> IsValidNextStatus(IContentEntity content, Status next);
Task<bool> CanUpdateAsync(IContentEntity content);
Task<Status2[]> GetNextsAsync(IContentEntity content);
Task<Status[]> GetNextsAsync(IContentEntity content);
Task<Status2[]> GetAllAsync(ISchemaEntity schema);
Task<Status[]> GetAllAsync(ISchemaEntity schema);
}
}

5
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)

2
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; }
}
}

2
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; }
}
}

2
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();
}

7
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);

4
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<ContentsController>(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<ContentsController>(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<ContentsController>(x => nameof(x.PutContent), values) + "?asDraft=true");
AddPutLink("draft/propose", controller.Url((ContentsController x) => nameof(x.PutContent), values) + "?asDraft=true");
}
AddPatchLink("patch", controller.Url<ContentsController>(x => nameof(x.PatchContent), values));

2
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs

@ -35,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// The possible statuses.
/// </summary>
[Required]
public Status2[] Statuses { get; set; }
public Status[] Statuses { get; set; }
public string ToEtag()
{

34
tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs

@ -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));
}
}
}

36
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();

6
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]

43
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<ISchemaEntity>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IContentRepository contentRepository = A.Dummy<IContentRepository>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE);
@ -100,9 +102,15 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.ReturnsLazily(x => x.GetArgument<ScriptContext>(0).Data);
A.CallTo(() => contentWorkflow.CanUpdateAsync(A<IContentEntity>.Ignored))
.Returns(true);
A.CallTo(() => contentWorkflow.IsValidNextStatus(A<IContentEntity>.Ignored, A<Status>.Ignored))
.Returns(true);
patched = patch.MergeInto(data);
sut = new ContentGrain(Store, A.Dummy<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), scriptEngine, A.Dummy<IContentRepository>());
sut = new ContentGrain(Store, A.Dummy<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), 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<ScriptContext>.Ignored, "<create-script>"))
@ -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<ScriptContext>.Ignored, "<change-script>"))
@ -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<ScriptContext>.Ignored, "<change-script>"))
@ -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<ScriptContext>.Ignored, "<change-script>"))
@ -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<ScriptContext>.Ignored, "<change-script>"))
@ -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);

7
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<IContentEntity>();

24
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<IContentEntity>();

3
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;

120
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<ISchemaEntity>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
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<DomainException>(() => 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<DomainException>(() => 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<DomainException>(() => 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<DomainException>(() => 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<IContentEntity>.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<IContentEntity>();
A.CallTo(() => content.Status).Returns(status);
A.CallTo(() => content.IsPending).Returns(isPending);
return content;
}
}
}

4
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();

Loading…
Cancel
Save