mirror of https://github.com/Squidex/squidex.git
99 changed files with 2121 additions and 455 deletions
@ -0,0 +1,58 @@ |
|||
// ==========================================================================
|
|||
// MongoContentRepository_EventHandling.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Events.Assets; |
|||
using Squidex.Domain.Apps.Events.Contents; |
|||
using Squidex.Infrastructure.Dispatching; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents |
|||
{ |
|||
public partial class MongoContentRepository |
|||
{ |
|||
public string Name |
|||
{ |
|||
get { return GetType().Name; } |
|||
} |
|||
|
|||
public string EventsFilter |
|||
{ |
|||
get { return "^(content-)|(asset-)"; } |
|||
} |
|||
|
|||
public override Task ClearAsync() |
|||
{ |
|||
return TaskHelper.Done; |
|||
} |
|||
|
|||
public Task On(Envelope<IEvent> @event) |
|||
{ |
|||
return this.DispatchActionAsync(@event.Payload, @event.Headers); |
|||
} |
|||
|
|||
protected Task On(AssetDeleted @event) |
|||
{ |
|||
return Collection.UpdateManyAsync( |
|||
Filter.And( |
|||
Filter.AnyEq(x => x.ReferencedIds, @event.AssetId), |
|||
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.AssetId)), |
|||
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.AssetId)); |
|||
} |
|||
|
|||
protected Task On(ContentDeleted @event) |
|||
{ |
|||
return Collection.UpdateManyAsync( |
|||
Filter.And( |
|||
Filter.AnyEq(x => x.ReferencedIds, @event.ContentId), |
|||
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)), |
|||
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// ==========================================================================
|
|||
// MongoSchemaEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using MongoDB.Bson; |
|||
using MongoDB.Bson.Serialization.Attributes; |
|||
using Squidex.Domain.Apps.Entities.Schemas.State; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas |
|||
{ |
|||
public sealed class MongoSchemaEntity |
|||
{ |
|||
[BsonId] |
|||
[BsonElement] |
|||
[BsonRepresentation(BsonType.String)] |
|||
public string Id { get; set; } |
|||
|
|||
[BsonElement] |
|||
[BsonRequired] |
|||
public SchemaState State { get; set; } |
|||
|
|||
[BsonElement] |
|||
[BsonRequired] |
|||
public int Version { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
// ==========================================================================
|
|||
// MongoSchemaRepository.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Entities.Schemas.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Schemas.State; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Squidex.Infrastructure.States; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas |
|||
{ |
|||
public sealed class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, ISnapshotStore<SchemaState> |
|||
{ |
|||
public MongoSchemaRepository(IMongoDatabase database) |
|||
: base(database) |
|||
{ |
|||
} |
|||
|
|||
protected override string CollectionName() |
|||
{ |
|||
return "Snapshots_Schemas"; |
|||
} |
|||
|
|||
protected override async Task SetupCollectionAsync(IMongoCollection<MongoSchemaEntity> collection) |
|||
{ |
|||
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.AppId)); |
|||
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.State.Name)); |
|||
} |
|||
|
|||
public async Task<(SchemaState Value, long Version)> ReadAsync(string key) |
|||
{ |
|||
var existing = |
|||
await Collection.Find(x => x.Id == key) |
|||
.FirstOrDefaultAsync(); |
|||
|
|||
if (existing != null) |
|||
{ |
|||
return (existing.State, existing.Version); |
|||
} |
|||
|
|||
return (null, -1); |
|||
} |
|||
|
|||
public async Task<Guid> FindSchemaIdAsync(Guid appId, string name) |
|||
{ |
|||
var schemaEntity = |
|||
await Collection.Find(x => x.State.Name == name).Only(x => x.Id) |
|||
.FirstOrDefaultAsync(); |
|||
|
|||
return schemaEntity != null ? Guid.Parse(schemaEntity.Id) : Guid.Empty; |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<Guid>> QuerySchemaIdsAsync(Guid appId) |
|||
{ |
|||
var schemaEntities = |
|||
await Collection.Find(x => x.State.AppId == appId).Only(x => x.Id) |
|||
.ToListAsync(); |
|||
|
|||
return schemaEntities.Select(x => Guid.Parse(x.Id)).ToList(); |
|||
} |
|||
|
|||
public async Task WriteAsync(string key, SchemaState value, long oldVersion, long newVersion) |
|||
{ |
|||
try |
|||
{ |
|||
await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion, |
|||
Update |
|||
.Set(x => x.State, value) |
|||
.Set(x => x.Version, newVersion), |
|||
Upsert); |
|||
} |
|||
catch (MongoWriteException ex) |
|||
{ |
|||
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) |
|||
{ |
|||
var existingVersion = |
|||
await Collection.Find(x => x.Id == key).Only(x => x.Version) |
|||
.FirstOrDefaultAsync(); |
|||
|
|||
if (existingVersion != null) |
|||
{ |
|||
throw new InconsistentStateException(existingVersion.Version, oldVersion, ex); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// ==========================================================================
|
|||
// ChangeContentStatus.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// =========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
|||
{ |
|||
public sealed class ChangeContentStatus : ContentCommand |
|||
{ |
|||
public Status Status { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
// ==========================================================================
|
|||
// ContentCommand.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Security.Claims; |
|||
using Squidex.Infrastructure.Commands; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
|||
{ |
|||
public abstract class ContentCommand : SchemaCommand, IAggregateCommand |
|||
{ |
|||
public ClaimsPrincipal User { get; set; } |
|||
|
|||
public Guid ContentId { get; set; } |
|||
|
|||
Guid IAggregateCommand.AggregateId |
|||
{ |
|||
get { return ContentId; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// ==========================================================================
|
|||
// ContentDataCommand.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
|||
{ |
|||
public abstract class ContentDataCommand : ContentCommand |
|||
{ |
|||
public NamedContentData Data { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
// ==========================================================================
|
|||
// CreateContent.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
|||
{ |
|||
public sealed class CreateContent : ContentDataCommand |
|||
{ |
|||
public bool Publish { get; set; } |
|||
} |
|||
} |
|||
@ -1,16 +1,14 @@ |
|||
// ==========================================================================
|
|||
// IAssetEventConsumer.cs
|
|||
// DeleteContent.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Assets |
|||
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
|||
{ |
|||
public interface IAssetEventConsumer : IEventConsumer |
|||
public sealed class DeleteContent : ContentCommand |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// ==========================================================================
|
|||
// PatchContent.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
|||
{ |
|||
public sealed class PatchContent : ContentDataCommand |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// ==========================================================================
|
|||
// UpdateContent.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Commands |
|||
{ |
|||
public sealed class UpdateContent : ContentDataCommand |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,158 @@ |
|||
// ==========================================================================
|
|||
// ContentCommandMiddleware.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|||
using Squidex.Domain.Apps.Entities.Contents.Guards; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.Dispatching; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class ContentCommandMiddleware : ICommandMiddleware |
|||
{ |
|||
private readonly IAggregateHandler handler; |
|||
private readonly IAppProvider appProvider; |
|||
private readonly IAssetRepository assetRepository; |
|||
private readonly IContentRepository contentRepository; |
|||
private readonly IScriptEngine scriptEngine; |
|||
|
|||
public ContentCommandMiddleware( |
|||
IAggregateHandler handler, |
|||
IAppProvider appProvider, |
|||
IAssetRepository assetRepository, |
|||
IScriptEngine scriptEngine, |
|||
IContentRepository contentRepository) |
|||
{ |
|||
Guard.NotNull(handler, nameof(handler)); |
|||
Guard.NotNull(appProvider, nameof(appProvider)); |
|||
Guard.NotNull(scriptEngine, nameof(scriptEngine)); |
|||
Guard.NotNull(assetRepository, nameof(assetRepository)); |
|||
Guard.NotNull(contentRepository, nameof(contentRepository)); |
|||
|
|||
this.handler = handler; |
|||
this.appProvider = appProvider; |
|||
this.scriptEngine = scriptEngine; |
|||
this.assetRepository = assetRepository; |
|||
this.contentRepository = contentRepository; |
|||
} |
|||
|
|||
protected async Task On(CreateContent command, CommandContext context) |
|||
{ |
|||
await handler.CreateAsync<ContentDomainObject>(context, async content => |
|||
{ |
|||
GuardContent.CanCreate(command); |
|||
|
|||
var operationContext = await CreateContext(command, content, () => "Failed to create content."); |
|||
|
|||
if (command.Publish) |
|||
{ |
|||
await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published"); |
|||
} |
|||
|
|||
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create"); |
|||
await operationContext.EnrichAsync(); |
|||
await operationContext.ValidateAsync(false); |
|||
|
|||
content.Create(command); |
|||
|
|||
context.Complete(EntityCreatedResult.Create(command.Data, content.Version)); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(UpdateContent command, CommandContext context) |
|||
{ |
|||
await handler.UpdateAsync<ContentDomainObject>(context, async content => |
|||
{ |
|||
GuardContent.CanUpdate(command); |
|||
|
|||
var operationContext = await CreateContext(command, content, () => "Failed to update content."); |
|||
|
|||
await operationContext.ValidateAsync(true); |
|||
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update"); |
|||
|
|||
content.Update(command); |
|||
|
|||
context.Complete(new ContentDataChangedResult(content.State.Data, content.Version)); |
|||
}); |
|||
} |
|||
|
|||
protected async Task On(PatchContent command, CommandContext context) |
|||
{ |
|||
await handler.UpdateAsync<ContentDomainObject>(context, async content => |
|||
{ |
|||
GuardContent.CanPatch(command); |
|||
|
|||
var operationContext = await CreateContext(command, content, () => "Failed to patch content."); |
|||
|
|||
await operationContext.ValidateAsync(true); |
|||
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch"); |
|||
|
|||
content.Patch(command); |
|||
|
|||
context.Complete(new ContentDataChangedResult(content.State.Data, content.Version)); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(ChangeContentStatus command, CommandContext context) |
|||
{ |
|||
return handler.UpdateAsync<ContentDomainObject>(context, async content => |
|||
{ |
|||
GuardContent.CanChangeContentStatus(content.State.Status, command); |
|||
|
|||
var operationContext = await CreateContext(command, content, () => "Failed to patch content."); |
|||
|
|||
await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); |
|||
|
|||
content.ChangeStatus(command); |
|||
}); |
|||
} |
|||
|
|||
protected Task On(DeleteContent command, CommandContext context) |
|||
{ |
|||
return handler.UpdateAsync<ContentDomainObject>(context, async content => |
|||
{ |
|||
GuardContent.CanDelete(command); |
|||
|
|||
var operationContext = await CreateContext(command, content, () => "Failed to delete content."); |
|||
|
|||
await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete"); |
|||
|
|||
content.Delete(command); |
|||
}); |
|||
} |
|||
|
|||
public async Task HandleAsync(CommandContext context, Func<Task> next) |
|||
{ |
|||
if (!await this.DispatchActionAsync(context.Command, context)) |
|||
{ |
|||
await next(); |
|||
} |
|||
} |
|||
|
|||
private async Task<ContentOperationContext> CreateContext(ContentCommand command, ContentDomainObject content, Func<string> message) |
|||
{ |
|||
var operationContext = |
|||
await ContentOperationContext.CreateAsync( |
|||
contentRepository, |
|||
content, |
|||
command, |
|||
appProvider, |
|||
assetRepository, |
|||
scriptEngine, |
|||
message); |
|||
|
|||
return operationContext; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// ==========================================================================
|
|||
// ContentChangedResult.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure.Commands; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public sealed class ContentDataChangedResult : EntitySavedResult |
|||
{ |
|||
public NamedContentData Data { get; } |
|||
|
|||
public ContentDataChangedResult(NamedContentData data, long version) |
|||
: base(version) |
|||
{ |
|||
Data = data; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
// ==========================================================================
|
|||
// ContentDomainObject.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|||
using Squidex.Domain.Apps.Entities.Contents.State; |
|||
using Squidex.Domain.Apps.Events.Contents; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Squidex.Infrastructure.Reflection; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class ContentDomainObject : DomainObjectBase<ContentDomainObject, ContentState> |
|||
{ |
|||
public ContentDomainObject Create(CreateContent command) |
|||
{ |
|||
VerifyNotCreated(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); |
|||
|
|||
if (command.Publish) |
|||
{ |
|||
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published })); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public ContentDomainObject Delete(DeleteContent command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public ContentDomainObject ChangeStatus(ChangeContentStatus command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public ContentDomainObject Update(UpdateContent command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
if (!command.Data.Equals(State.Data)) |
|||
{ |
|||
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated())); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
|
|||
public ContentDomainObject Patch(PatchContent command) |
|||
{ |
|||
VerifyCreatedAndNotDeleted(); |
|||
|
|||
var newData = State.Data.MergeInto(command.Data); |
|||
|
|||
if (!newData.Equals(State.Data)) |
|||
{ |
|||
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated { Data = newData })); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
|
|||
private void VerifyNotCreated() |
|||
{ |
|||
if (State.Data != null) |
|||
{ |
|||
throw new DomainException("Content has already been created."); |
|||
} |
|||
} |
|||
|
|||
private void VerifyCreatedAndNotDeleted() |
|||
{ |
|||
if (State.IsDeleted || State.Data == null) |
|||
{ |
|||
throw new DomainException("Content has already been deleted or not created yet."); |
|||
} |
|||
} |
|||
|
|||
protected override void OnRaised(Envelope<IEvent> @event) |
|||
{ |
|||
UpdateState(State.Apply(@event)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
// ==========================================================================
|
|||
// ContentOperationContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.EnrichContent; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Core.ValidateContent; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public sealed class ContentOperationContext |
|||
{ |
|||
private ContentDomainObject content; |
|||
private ContentCommand command; |
|||
private IContentRepository contentRepository; |
|||
private IAssetRepository assetRepository; |
|||
private IScriptEngine scriptEngine; |
|||
private ISchemaEntity schemaEntity; |
|||
private IAppEntity appEntity; |
|||
private Func<string> message; |
|||
|
|||
public static async Task<ContentOperationContext> CreateAsync( |
|||
IContentRepository contentRepository, |
|||
ContentDomainObject content, |
|||
ContentCommand command, |
|||
IAppProvider appProvider, |
|||
IAssetRepository assetRepository, |
|||
IScriptEngine scriptEngine, |
|||
Func<string> message) |
|||
{ |
|||
var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id); |
|||
|
|||
var context = new ContentOperationContext |
|||
{ |
|||
appEntity = appEntity, |
|||
assetRepository = assetRepository, |
|||
contentRepository = contentRepository, |
|||
content = content, |
|||
command = command, |
|||
message = message, |
|||
schemaEntity = schemaEntity, |
|||
scriptEngine = scriptEngine |
|||
}; |
|||
|
|||
return context; |
|||
} |
|||
|
|||
public Task EnrichAsync() |
|||
{ |
|||
if (command is ContentDataCommand dataCommand) |
|||
{ |
|||
dataCommand.Data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver()); |
|||
} |
|||
|
|||
return TaskHelper.Done; |
|||
} |
|||
|
|||
public async Task ValidateAsync(bool partial) |
|||
{ |
|||
if (command is ContentDataCommand dataCommand) |
|||
{ |
|||
var errors = new List<ValidationError>(); |
|||
|
|||
var appId = command.AppId.Id; |
|||
|
|||
var ctx = |
|||
new ValidationContext( |
|||
(contentIds, schemaId) => |
|||
{ |
|||
return QueryContentsAsync(appId, schemaId, contentIds); |
|||
}, |
|||
assetIds => |
|||
{ |
|||
return QueryAssetsAsync(appId, assetIds); |
|||
}); |
|||
|
|||
if (partial) |
|||
{ |
|||
await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); |
|||
} |
|||
else |
|||
{ |
|||
await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); |
|||
} |
|||
|
|||
if (errors.Count > 0) |
|||
{ |
|||
throw new ValidationException(message(), errors.ToArray()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(Guid appId, IEnumerable<Guid> assetIds) |
|||
{ |
|||
return await assetRepository.QueryAsync(appId, null, new HashSet<Guid>(assetIds), null, int.MaxValue, 0); |
|||
} |
|||
|
|||
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable<Guid> contentIds) |
|||
{ |
|||
return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList()); |
|||
} |
|||
|
|||
public Task ExecuteScriptAndTransformAsync(Func<ISchemaEntity, string> script, object operation) |
|||
{ |
|||
if (command is ContentDataCommand dataCommand) |
|||
{ |
|||
var ctx = new ScriptContext { ContentId = content.State.Id, OldData = content.State.Data, User = command.User, Operation = operation.ToString(), Data = dataCommand.Data }; |
|||
|
|||
dataCommand.Data = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity)); |
|||
} |
|||
|
|||
return TaskHelper.Done; |
|||
} |
|||
|
|||
public Task ExecuteScriptAsync(Func<ISchemaEntity, string> script, object operation) |
|||
{ |
|||
var ctx = new ScriptContext { ContentId = content.State.Id, OldData = content.State.Data, User = command.User, Operation = operation.ToString() }; |
|||
|
|||
scriptEngine.Execute(ctx, script(schemaEntity)); |
|||
|
|||
return TaskHelper.Done; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// ==========================================================================
|
|||
// GuardContent.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Guards |
|||
{ |
|||
public static class GuardContent |
|||
{ |
|||
public static void CanCreate(CreateContent command) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
Validate.It(() => "Cannot created content.", error => |
|||
{ |
|||
if (command.Data == null) |
|||
{ |
|||
error(new ValidationError("Data cannot be null.", nameof(command.Data))); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public static void CanUpdate(UpdateContent command) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
Validate.It(() => "Cannot update content.", error => |
|||
{ |
|||
if (command.Data == null) |
|||
{ |
|||
error(new ValidationError("Data cannot be null.", nameof(command.Data))); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public static void CanPatch(PatchContent command) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
Validate.It(() => "Cannot patch content.", error => |
|||
{ |
|||
if (command.Data == null) |
|||
{ |
|||
error(new ValidationError("Data cannot be null.", nameof(command.Data))); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public static void CanChangeContentStatus(Status status, ChangeContentStatus command) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
Validate.It(() => "Cannot change status.", error => |
|||
{ |
|||
if (!StatusFlow.Exists(command.Status) || !StatusFlow.CanChange(status, command.Status)) |
|||
{ |
|||
error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status))); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public static void CanDelete(DeleteContent command) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,244 @@ |
|||
// ==========================================================================
|
|||
// ContentCommandMiddlewareTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class ContentCommandMiddlewareTests : HandlerTestBase<ContentDomainObject> |
|||
{ |
|||
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>(); |
|||
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); |
|||
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
|||
private readonly IAppEntity app = A.Fake<IAppEntity>(); |
|||
private readonly ClaimsPrincipal user = new ClaimsPrincipal(); |
|||
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); |
|||
private readonly Guid contentId = Guid.NewGuid(); |
|||
private readonly ContentDomainObject content = new ContentDomainObject(); |
|||
private readonly ContentCommandMiddleware sut; |
|||
|
|||
private readonly NamedContentData invalidData = |
|||
new NamedContentData() |
|||
.AddField("my-field1", new ContentFieldData() |
|||
.AddValue(null)) |
|||
.AddField("my-field2", new ContentFieldData() |
|||
.AddValue(1)); |
|||
private readonly NamedContentData data = |
|||
new NamedContentData() |
|||
.AddField("my-field1", new ContentFieldData() |
|||
.AddValue(1)) |
|||
.AddField("my-field2", new ContentFieldData() |
|||
.AddValue(1)); |
|||
private readonly NamedContentData patch = |
|||
new NamedContentData() |
|||
.AddField("my-field1", new ContentFieldData() |
|||
.AddValue(1)); |
|||
|
|||
public ContentCommandMiddlewareTests() |
|||
{ |
|||
var schemaDef = |
|||
new Schema("my-schema") |
|||
.AddField(new NumberField(1, "my-field1", Partitioning.Invariant, |
|||
new NumberFieldProperties { IsRequired = true })) |
|||
.AddField(new NumberField(2, "my-field2", Partitioning.Invariant, |
|||
new NumberFieldProperties { IsRequired = false })); |
|||
|
|||
sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy<IAssetRepository>(), scriptEngine, A.Dummy<IContentRepository>()); |
|||
|
|||
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); |
|||
|
|||
A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app); |
|||
|
|||
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
|||
A.CallTo(() => schema.ScriptCreate).Returns("<create-script>"); |
|||
A.CallTo(() => schema.ScriptChange).Returns("<change-script>"); |
|||
A.CallTo(() => schema.ScriptUpdate).Returns("<update-script>"); |
|||
A.CallTo(() => schema.ScriptDelete).Returns("<delete-script>"); |
|||
|
|||
A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)).Returns((app, schema)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_throw_exception_if_data_is_not_valid() |
|||
{ |
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)) |
|||
.Returns(invalidData); |
|||
|
|||
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user }); |
|||
|
|||
await TestCreate(content, async _ => |
|||
{ |
|||
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context)); |
|||
}, false); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_create_content() |
|||
{ |
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)) |
|||
.Returns(data); |
|||
|
|||
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user }); |
|||
|
|||
await TestCreate(content, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
Assert.Equal(data, context.Result<EntityCreatedResult<NamedContentData>>().IdOrValue); |
|||
|
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>")).MustHaveHappened(); |
|||
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_also_invoke_publish_script_when_publishing() |
|||
{ |
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)) |
|||
.Returns(data); |
|||
|
|||
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user, Publish = true }); |
|||
|
|||
await TestCreate(content, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
Assert.Equal(data, context.Result<EntityCreatedResult<NamedContentData>>().IdOrValue); |
|||
|
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>")).MustHaveHappened(); |
|||
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_throw_exception_if_data_is_not_valid() |
|||
{ |
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)) |
|||
.Returns(invalidData); |
|||
|
|||
CreateContent(); |
|||
|
|||
var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user }); |
|||
|
|||
await TestUpdate(content, async _ => |
|||
{ |
|||
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context)); |
|||
}, false); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Update_should_update_domain_object() |
|||
{ |
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)) |
|||
.Returns(data); |
|||
|
|||
CreateContent(); |
|||
|
|||
var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data, User = user }); |
|||
|
|||
await TestUpdate(content, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
Assert.Equal(data, context.Result<ContentDataChangedResult>().Data); |
|||
|
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>")).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Patch_should_throw_exception_if_data_is_not_valid() |
|||
{ |
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)) |
|||
.Returns(invalidData); |
|||
|
|||
CreateContent(); |
|||
|
|||
var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user }); |
|||
|
|||
await TestUpdate(content, async _ => |
|||
{ |
|||
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context)); |
|||
}, false); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Patch_should_update_domain_object() |
|||
{ |
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)) |
|||
.Returns(data); |
|||
|
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)).Returns(patch); |
|||
|
|||
CreateContent(); |
|||
|
|||
var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = patch, User = user }); |
|||
|
|||
await TestUpdate(content, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
Assert.NotNull(context.Result<ContentDataChangedResult>().Data); |
|||
|
|||
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>")).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ChangeStatus_should_publish_domain_object() |
|||
{ |
|||
CreateContent(); |
|||
|
|||
var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published }); |
|||
|
|||
await TestUpdate(content, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Delete_should_update_domain_object() |
|||
{ |
|||
CreateContent(); |
|||
|
|||
var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user }); |
|||
|
|||
await TestUpdate(content, async _ => |
|||
{ |
|||
await sut.HandleAsync(command); |
|||
}); |
|||
|
|||
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<delete-script>")).MustHaveHappened(); |
|||
} |
|||
|
|||
private void CreateContent() |
|||
{ |
|||
content.Create(new CreateContent { Data = data }); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,269 @@ |
|||
// ==========================================================================
|
|||
// ContentDomainObjectTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using FluentAssertions; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Domain.Apps.Events.Contents; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class ContentDomainObjectTests : HandlerTestBase<ContentDomainObject> |
|||
{ |
|||
private readonly NamedContentData data = |
|||
new NamedContentData() |
|||
.AddField("field1", |
|||
new ContentFieldData() |
|||
.AddValue("iv", 1)); |
|||
private readonly NamedContentData otherData = |
|||
new NamedContentData() |
|||
.AddField("field2", |
|||
new ContentFieldData() |
|||
.AddValue("iv", 2)); |
|||
private readonly Guid contentId = Guid.NewGuid(); |
|||
private readonly ContentDomainObject sut = new ContentDomainObject(); |
|||
|
|||
[Fact] |
|||
public void Create_should_throw_exception_if_created() |
|||
{ |
|||
sut.Create(new CreateContent { Data = data }); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Create(CreateContentCommand(new CreateContent { Data = data })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Create_should_create_events() |
|||
{ |
|||
sut.Create(CreateContentCommand(new CreateContent { Data = data })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateContentEvent(new ContentCreated { Data = data }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Create_should_also_publish_if_set_to_true() |
|||
{ |
|||
sut.Create(CreateContentCommand(new CreateContent { Data = data, Publish = true })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateContentEvent(new ContentCreated { Data = data }), |
|||
CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Update(CreateContentCommand(new UpdateContent { Data = data })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_throw_exception_if_content_is_deleted() |
|||
{ |
|||
CreateContent(); |
|||
DeleteContent(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Update(CreateContentCommand(new UpdateContent())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_create_events() |
|||
{ |
|||
CreateContent(); |
|||
|
|||
sut.Update(CreateContentCommand(new UpdateContent { Data = otherData })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateContentEvent(new ContentUpdated { Data = otherData }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Update_should_not_create_event_for_same_data() |
|||
{ |
|||
CreateContent(); |
|||
UpdateContent(); |
|||
|
|||
sut.Update(CreateContentCommand(new UpdateContent { Data = data })); |
|||
|
|||
sut.GetUncomittedEvents().Should().BeEmpty(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Patch_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Patch(CreateContentCommand(new PatchContent { Data = data })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Patch_should_throw_exception_if_content_is_deleted() |
|||
{ |
|||
CreateContent(); |
|||
DeleteContent(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Patch(CreateContentCommand(new PatchContent())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Patch_should_create_events() |
|||
{ |
|||
CreateContent(); |
|||
|
|||
sut.Patch(CreateContentCommand(new PatchContent { Data = otherData })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateContentEvent(new ContentUpdated { Data = otherData }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Patch_should_not_create_event_for_same_data() |
|||
{ |
|||
CreateContent(); |
|||
UpdateContent(); |
|||
|
|||
sut.Patch(CreateContentCommand(new PatchContent { Data = data })); |
|||
|
|||
sut.GetUncomittedEvents().Should().BeEmpty(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ChangeStatus_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ChangeStatus_should_throw_exception_if_content_is_deleted() |
|||
{ |
|||
CreateContent(); |
|||
DeleteContent(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ChangeStatus_should_refresh_properties_and_create_events() |
|||
{ |
|||
CreateContent(); |
|||
|
|||
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); |
|||
|
|||
Assert.Equal(Status.Published, sut.State.Status); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Delete(CreateContentCommand(new DeleteContent())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_throw_exception_if_already_deleted() |
|||
{ |
|||
CreateContent(); |
|||
DeleteContent(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Delete(CreateContentCommand(new DeleteContent())); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Delete_should_update_properties_and_create_events() |
|||
{ |
|||
CreateContent(); |
|||
|
|||
sut.Delete(CreateContentCommand(new DeleteContent())); |
|||
|
|||
Assert.True(sut.State.IsDeleted); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateContentEvent(new ContentDeleted()) |
|||
); |
|||
} |
|||
|
|||
private void CreateContent() |
|||
{ |
|||
sut.Create(CreateContentCommand(new CreateContent { Data = data })); |
|||
sut.ClearUncommittedEvents(); |
|||
} |
|||
|
|||
private void UpdateContent() |
|||
{ |
|||
sut.Update(CreateContentCommand(new UpdateContent { Data = data })); |
|||
sut.ClearUncommittedEvents(); |
|||
} |
|||
|
|||
private void ChangeStatus(Status status) |
|||
{ |
|||
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status })); |
|||
sut.ClearUncommittedEvents(); |
|||
} |
|||
|
|||
private void DeleteContent() |
|||
{ |
|||
sut.Delete(CreateContentCommand(new DeleteContent())); |
|||
sut.ClearUncommittedEvents(); |
|||
} |
|||
|
|||
protected T CreateContentEvent<T>(T @event) where T : ContentEvent |
|||
{ |
|||
@event.ContentId = contentId; |
|||
|
|||
return CreateEvent(@event); |
|||
} |
|||
|
|||
protected T CreateContentCommand<T>(T command) where T : ContentCommand |
|||
{ |
|||
command.ContentId = contentId; |
|||
|
|||
return CreateCommand(command); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
// ==========================================================================
|
|||
// ContentEventTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Domain.Apps.Events.Contents; |
|||
using Squidex.Domain.Apps.Events.Contents.Old; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
#pragma warning disable CS0612 // Type or member is obsolete
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class ContentEventTests |
|||
{ |
|||
private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); |
|||
private readonly NamedId<Guid> appId = new NamedId<Guid>(Guid.NewGuid(), "my-app"); |
|||
private readonly NamedId<Guid> schemaId = new NamedId<Guid>(Guid.NewGuid(), "my-schema"); |
|||
private readonly Guid contentId = Guid.NewGuid(); |
|||
|
|||
[Fact] |
|||
public void Should_migrate_content_published_to_content_status_changed() |
|||
{ |
|||
var source = CreateEvent(new ContentPublished()); |
|||
|
|||
source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Published })); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_migrate_content_unpublished_to_content_status_changed() |
|||
{ |
|||
var source = CreateEvent(new ContentUnpublished()); |
|||
|
|||
source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_migrate_content_restored_to_content_status_changed() |
|||
{ |
|||
var source = CreateEvent(new ContentRestored()); |
|||
|
|||
source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_migrate_content_archived_to_content_status_changed() |
|||
{ |
|||
var source = CreateEvent(new ContentArchived()); |
|||
|
|||
source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Archived })); |
|||
} |
|||
|
|||
private T CreateEvent<T>(T contentEvent) where T : ContentEvent |
|||
{ |
|||
contentEvent.Actor = actor; |
|||
contentEvent.AppId = appId; |
|||
contentEvent.SchemaId = schemaId; |
|||
contentEvent.ContentId = contentId; |
|||
|
|||
return contentEvent; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
// ==========================================================================
|
|||
// GuardContentTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Entities.Contents.Commands; |
|||
using Squidex.Domain.Apps.Entities.Contents.Guards; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Guard |
|||
{ |
|||
public class GuardContentTests |
|||
{ |
|||
[Fact] |
|||
public void CanCreate_should_throw_exception_if_data_is_null() |
|||
{ |
|||
var command = new CreateContent(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardContent.CanCreate(command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanCreate_should_not_throw_exception_if_data_is_not_null() |
|||
{ |
|||
var command = new CreateContent { Data = new NamedContentData() }; |
|||
|
|||
GuardContent.CanCreate(command); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanUpdate_should_throw_exception_if_data_is_null() |
|||
{ |
|||
var command = new UpdateContent(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardContent.CanUpdate(command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanUpdate_should_not_throw_exception_if_data_is_not_null() |
|||
{ |
|||
var command = new UpdateContent { Data = new NamedContentData() }; |
|||
|
|||
GuardContent.CanUpdate(command); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanPatch_should_throw_exception_if_data_is_null() |
|||
{ |
|||
var command = new PatchContent(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardContent.CanPatch(command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanPatch_should_not_throw_exception_if_data_is_not_null() |
|||
{ |
|||
var command = new PatchContent { Data = new NamedContentData() }; |
|||
|
|||
GuardContent.CanPatch(command); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanChangeContentStatus_should_throw_exception_if_status_not_valid() |
|||
{ |
|||
var command = new ChangeContentStatus { Status = (Status)10 }; |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanChangeContentStatus_should_throw_exception_if_status_flow_not_valid() |
|||
{ |
|||
var command = new ChangeContentStatus { Status = Status.Published }; |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanChangeContentStatus_not_should_throw_exception_if_status_flow_valid() |
|||
{ |
|||
var command = new ChangeContentStatus { Status = Status.Published }; |
|||
|
|||
GuardContent.CanChangeContentStatus(Status.Draft, command); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanPatch_should_not_throw_exception() |
|||
{ |
|||
var command = new DeleteContent(); |
|||
|
|||
GuardContent.CanDelete(command); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue