Browse Source

Started with API end service.

pull/329/head
Sebastian Stehle 7 years ago
parent
commit
762728dd1d
  1. 32
      src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
  2. 25
      src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs
  3. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs
  4. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs
  5. 18
      src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs
  6. 127
      src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs
  7. 96
      src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs
  8. 88
      src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs
  9. 18
      src/Squidex.Domain.Apps.Entities/Comments/ICommentGrain.cs
  10. 13
      src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs
  11. 4
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  12. 20
      src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs
  13. 18
      src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs
  14. 20
      src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs
  15. 16
      src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs
  16. 51
      src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  17. 4
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  18. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs
  19. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/UpdateAssetDto.cs
  20. 128
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  21. 53
      src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs
  22. 48
      src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs
  23. 33
      src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs
  24. 5
      src/Squidex/Config/Domain/EntitiesServices.cs
  25. 2
      src/Squidex/app/shared/module.ts
  26. 2
      src/Squidex/app/shared/services/assets.service.ts
  27. 133
      src/Squidex/app/shared/services/comments.service.spec.ts
  28. 125
      src/Squidex/app/shared/services/comments.service.ts
  29. 162
      tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs
  30. 164
      tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs

32
src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using NodaTime;
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Comments
{
public sealed class Comment
{
public Guid Id { get; }
public Instant Time { get; }
public RefToken User { get; }
public string Text { get; }
public Comment(Guid id, Instant time, RefToken user, string text)
{
Id = id;
Time = time;
Text = text;
User = user;
}
}
}

25
src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs

@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public abstract class CommentsCommand : SquidexCommand, IAggregateCommand, IAppCommand
{
public Guid CommentsId { get; set; }
public NamedId<Guid> AppId { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return CommentsId; }
}
}
}

18
src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public sealed class CreateComment : CommentsCommand
{
public Guid CommentId { get; } = Guid.NewGuid();
public string Text { get; set; }
}
}

18
src/Squidex.Domain.Apps.Entities/Comments/Commands/DeleteComment.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public sealed class DeleteComment : CommentsCommand
{
public Guid CommentId { get; set; }
public string Text { get; set; }
}
}

18
src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public sealed class UpdateComment : CommentsCommand
{
public Guid CommentId { get; set; }
public string Text { get; set; }
}
}

127
src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs

@ -0,0 +1,127 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.Comments.Guards;
using Squidex.Domain.Apps.Entities.Comments.State;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Comments
{
public sealed class CommentsGrain : DomainObjectGrainBase<CommentsState>, ICommentGrain
{
private readonly IStore<Guid> store;
private readonly List<Envelope<CommentsEvent>> events = new List<Envelope<CommentsEvent>>();
private CommentsState snapshot = new CommentsState { Version = EtagVersion.Empty };
private IPersistence persistence;
public override CommentsState Snapshot
{
get { return snapshot; }
}
public CommentsGrain(IStore<Guid> store, ISemanticLog log)
: base(log)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
protected override void ApplyEvent(Envelope<IEvent> @event)
{
snapshot = new CommentsState { Version = snapshot.Version + 1 };
events.Add(@event.To<CommentsEvent>());
}
protected override void RestorePreviousSnapshot(CommentsState previousSnapshot, long previousVersion)
{
snapshot = previousSnapshot;
}
protected override Task ReadAsync(Type type, Guid id)
{
persistence = store.WithEventSourcing<Guid>(GetType(), id, ApplyEvent);
return persistence.ReadAsync();
}
protected override async Task WriteAsync(Envelope<IEvent>[] events, long previousVersion)
{
if (events.Length > 0)
{
await persistence.WriteEventsAsync(events);
await persistence.WriteSnapshotAsync(Snapshot);
}
}
protected override Task<object> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{
case CreateComment createComment:
return UpsertAsync(createComment, c =>
{
GuardComments.CanCreate(c);
Create(c);
return EntityCreatedResult.Create(createComment.CommentId, Version);
});
case UpdateComment updateComment:
return UpsertAsync(updateComment, c =>
{
GuardComments.CanUpdate(events, c);
Update(c);
});
case DeleteComment deleteComment:
return UpsertAsync(deleteComment, c =>
{
GuardComments.CanDelete(events, c);
Delete(c);
});
default:
throw new NotSupportedException();
}
}
public void Create(CreateComment command)
{
RaiseEvent(SimpleMapper.Map(command, new CommentCreated()));
}
public void Update(UpdateComment command)
{
RaiseEvent(SimpleMapper.Map(command, new CommentUpdated()));
}
public void Delete(DeleteComment command)
{
RaiseEvent(SimpleMapper.Map(command, new CommentDeleted()));
}
public Task<CommentsResult> GetCommentsAsync(long version = EtagVersion.Any)
{
return Task.FromResult(CommentsResult.FromEvents(events, Version, (int)version));
}
}
}

96
src/Squidex.Domain.Apps.Entities/Comments/CommentsResult.cs

@ -0,0 +1,96 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Comments;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Comments
{
public sealed class CommentsResult
{
public List<Comment> CreatedComments { get; set; } = new List<Comment>();
public List<Comment> UpdatedComments { get; set; } = new List<Comment>();
public List<Guid> DeletedComments { get; set; } = new List<Guid>();
public long Version { get; set; }
public static CommentsResult FromEvents(IEnumerable<Envelope<CommentsEvent>> events, long currentVersion, int lastVersion)
{
var result = new CommentsResult { Version = currentVersion };
foreach (var @event in events.Skip(lastVersion < 0 ? 0 : lastVersion + 1))
{
switch (@event.Payload)
{
case CommentDeleted deleted:
{
var id = deleted.CommentId;
if (result.CreatedComments.Any(x => x.Id == id))
{
result.CreatedComments.RemoveAll(x => x.Id == id);
}
else if (result.UpdatedComments.Any(x => x.Id == id))
{
result.UpdatedComments.RemoveAll(x => x.Id == id);
result.DeletedComments.Add(id);
}
else
{
result.DeletedComments.Add(id);
}
break;
}
case CommentCreated created:
{
var comment = new Comment(
created.CommentId,
@event.Headers.Timestamp(),
@event.Payload.Actor,
created.Text);
result.CreatedComments.Add(comment);
break;
}
case CommentUpdated updated:
{
var id = updated.CommentId;
var comment = new Comment(
id,
@event.Headers.Timestamp(),
@event.Payload.Actor,
updated.Text);
if (result.CreatedComments.Any(x => x.Id == id))
{
result.CreatedComments.RemoveAll(x => x.Id == id);
result.CreatedComments.Add(comment);
}
else
{
result.UpdatedComments.Add(comment);
}
break;
}
}
}
return result;
}
}
}

88
src/Squidex.Domain.Apps.Entities/Comments/Guards/GuardComments.cs

@ -0,0 +1,88 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Comments.Guards
{
public static class GuardComments
{
public static void CanCreate(CreateComment command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot create comment.", e =>
{
if (string.IsNullOrWhiteSpace(command.Text))
{
e("Text is required.", nameof(command.Text));
}
});
}
public static void CanUpdate(List<Envelope<CommentsEvent>> events, UpdateComment command)
{
Guard.NotNull(command, nameof(command));
var comment = FindComment(events, command.CommentId);
if (!comment.Payload.Actor.Equals(command.Actor))
{
throw new DomainException("Comment is created by another actor.");
}
Validate.It(() => "Cannot update comment.", e =>
{
if (string.IsNullOrWhiteSpace(command.Text))
{
e("Text is required.", nameof(command.Text));
}
});
}
public static void CanDelete(List<Envelope<CommentsEvent>> events, DeleteComment command)
{
Guard.NotNull(command, nameof(command));
var comment = FindComment(events, command.CommentId);
if (!comment.Payload.Actor.Equals(command.Actor))
{
throw new DomainException("Comment is created by another actor.");
}
}
private static Envelope<CommentCreated> FindComment(List<Envelope<CommentsEvent>> events, Guid commentId)
{
Envelope<CommentCreated> result = null;
foreach (var @event in events)
{
if (@event.Payload is CommentCreated created && created.CommentId == commentId)
{
result = @event.To<CommentCreated>();
}
else if (@event.Payload is CommentDeleted deleted && deleted.CommentId == commentId)
{
result = null;
}
}
if (result == null)
{
throw new DomainObjectNotFoundException(commentId.ToString(), "Comments", typeof(CommentsGrain));
}
return result;
}
}
}

18
src/Squidex.Domain.Apps.Entities/Comments/ICommentGrain.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Comments
{
public interface ICommentGrain : IDomainObjectGrain
{
Task<CommentsResult> GetCommentsAsync(long version = EtagVersion.Any);
}
}

13
src/Squidex.Domain.Apps.Entities/Comments/State/CommentsState.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Comments.State
{
public sealed class CommentsState : DomainObjectState<CommentsState>
{
}
}

4
src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs

@ -28,8 +28,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Instant LastModified { get; set; }
public Status Status { get; set; }
public ScheduleJob ScheduleJob { get; set; }
public RefToken CreatedBy { get; set; }
@ -40,6 +38,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
public NamedContentData DataDraft { get; set; }
public Status Status { get; set; }
public bool IsPending { get; set; }
public static ContentEntity Create(CreateContent command, EntityCreatedResult<NamedContentData> result)

20
src/Squidex.Domain.Apps.Events/Comments/CommentCreated.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Comments
{
[EventType(nameof(CommentCreated))]
public sealed class CommentCreated : CommentsEvent
{
public Guid CommentId { get; set; }
public string Text { get; set; }
}
}

18
src/Squidex.Domain.Apps.Events/Comments/CommentDeleted.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Comments
{
[EventType(nameof(CommentCreated))]
public sealed class CommentDeleted : CommentsEvent
{
public Guid CommentId { get; set; }
}
}

20
src/Squidex.Domain.Apps.Events/Comments/CommentUpdated.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Comments
{
[EventType(nameof(CommentUpdated))]
public sealed class CommentUpdated : CommentsEvent
{
public Guid CommentId { get; set; }
public string Text { get; set; }
}
}

16
src/Squidex.Domain.Apps.Events/Comments/CommentsEvent.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Events.Comments
{
public abstract class CommentsEvent : AppEvent
{
public Guid CommentsId { get; set; }
}
}

51
src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs

@ -21,6 +21,13 @@ namespace Squidex.Infrastructure.Commands
private readonly ISemanticLog log;
private Guid id;
private enum Mode
{
Create,
Update,
Upsert
}
public Guid Id
{
get { return id; }
@ -81,45 +88,65 @@ namespace Squidex.Infrastructure.Commands
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, false);
return InvokeAsync(command, handler, Mode.Create);
}
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), false);
return InvokeAsync(command, handler?.ToAsync(), Mode.Create);
}
protected Task<object> CreateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler.ToDefault<TCommand, object>(), false);
return InvokeAsync(command, handler.ToDefault<TCommand, object>(), Mode.Create);
}
protected Task<object> CreateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), false);
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Create);
}
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, true);
return InvokeAsync(command, handler, Mode.Update);
}
protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), true);
return InvokeAsync(command, handler?.ToAsync(), Mode.Update);
}
protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), true);
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), Mode.Update);
}
protected Task<object> UpdateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), true);
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Update);
}
protected Task<object> UpsertReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, Mode.Upsert);
}
protected Task<object> UpsertAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), Mode.Upsert);
}
protected Task<object> UpsertAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), Mode.Upsert);
}
protected Task<object> UpsertAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), Mode.Upsert);
}
private async Task<object> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler, bool isUpdate) where TCommand : class, IAggregateCommand
private async Task<object> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler, Mode mode) where TCommand : class, IAggregateCommand
{
Guard.NotNull(command, nameof(command));
@ -128,7 +155,7 @@ namespace Squidex.Infrastructure.Commands
throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion);
}
if (isUpdate && Version < 0)
if (mode == Mode.Update && Version < 0)
{
try
{
@ -141,7 +168,7 @@ namespace Squidex.Infrastructure.Commands
throw new DomainObjectNotFoundException(id.ToString(), GetType());
}
if (!isUpdate && Version >= 0)
if (mode == Mode.Create && Version >= 0)
{
throw new DomainException("Object has already been created.");
}
@ -158,7 +185,7 @@ namespace Squidex.Infrastructure.Commands
if (result == null)
{
if (isUpdate)
if (Version > 0)
{
result = new EntitySavedResult(Version);
}

4
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -215,7 +215,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
var context = await CommandBus.PublishAsync(command);
var result = context.Result<AssetSavedResult>();
var response = AssetReplacedDto.Create(command, result);
var response = AssetReplacedDto.FromCommand(command, result);
return StatusCode(201, response);
}
@ -236,7 +236,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(1)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] AssetUpdateDto request)
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] UpdateAssetDto request)
{
await CommandBus.PublishAsync(request.ToCommand(id));

2
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetReplacedDto.cs

@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary>
public long Version { get; set; }
public static AssetReplacedDto Create(UpdateAsset command, AssetSavedResult result)
public static AssetReplacedDto FromCommand(UpdateAsset command, AssetSavedResult result)
{
var response = new AssetReplacedDto
{

2
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetUpdateDto.cs → src/Squidex/Areas/Api/Controllers/Assets/Models/UpdateAssetDto.cs

@ -12,7 +12,7 @@ using Squidex.Domain.Apps.Entities.Assets.Commands;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetUpdateDto
public sealed class UpdateAssetDto
{
/// <summary>
/// The new name of the asset.

128
src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -0,0 +1,128 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using Squidex.Areas.Api.Controllers.Comments.Models;
using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Comments
{
/// <summary>
/// Manages comments for any kind of resource.
/// </summary>
[ApiExceptionFilter]
[ApiExplorerSettings(GroupName = nameof(Languages))]
public sealed class CommentsController : ApiController
{
private readonly IGrainFactory grainFactory;
public CommentsController(ICommandBus commandBus, IGrainFactory grainFactory)
: base(commandBus)
{
this.grainFactory = grainFactory;
}
/// <summary>
/// Get all comments.
/// </summary>
/// <param name="commentsId">The id of the comments.</param>
/// <returns>
/// 200 => All comments returned.
/// </returns>
[HttpGet]
[Route("comments/{commentsId}")]
[ProducesResponseType(typeof(CommentsDto), 200)]
[ApiCosts(0)]
public async Task<IActionResult> GetComments(Guid commentsId)
{
if (!int.TryParse(Request.Headers["X-Since"], out var version))
{
version = -1;
}
var result = await grainFactory.GetGrain<ICommentGrain>(commentsId).GetCommentsAsync(version);
var response = CommentsDto.FromResult(result);
Response.Headers["ETag"] = response.Version.ToString();
return Ok(response);
}
/// <summary>
/// Create a new comment.
/// </summary>
/// <param name="commentsId">The id of the comments.</param>
/// <param name="request">The comment object that needs to created.</param>
/// <returns>
/// 201 => Comment created.
/// 400 => Comment is not valid.
/// </returns>
[HttpPost]
[Route("comments/{commentdsId}")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(0)]
public async Task<IActionResult> PostComment(Guid commentsId, [FromBody] UpsertCommentDto request)
{
var command = request.ToCreateCommand(commentsId);
var context = await CommandBus.PublishAsync(command);
var response = CommentDto.FromCommand(command);
return CreatedAtAction(nameof(GetComments), new { commentsId }, response);
}
/// <summary>
/// Updates the comment.
/// </summary>
/// <param name="commentsId">The id of the comments.</param>
/// <param name="commentId">The id of the comment.</param>
/// <param name="request">The comment object that needs to updated.</param>
/// <returns>
/// 204 => Comment updated.
/// 400 => Comment text not valid.
/// 404 => Comment not found.
/// </returns>
[MustBeAppReader]
[HttpPut]
[Route("comments/{commentdsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(0)]
public async Task<IActionResult> PutComment(Guid commentsId, Guid commentId, [FromBody] UpsertCommentDto request)
{
await CommandBus.PublishAsync(request.ToUpdateComment(commentsId, commentId));
return NoContent();
}
/// <summary>
/// Deletes the comment.
/// </summary>
/// <param name="commentsId">The id of the comments.</param>
/// <param name="commentId">The id of the comment.</param>
/// <returns>
/// 204 => Comment deleted.
/// 404 => Comment not found.
/// </returns>
[HttpDelete]
[Route("comments/{commentdsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteComment(Guid commentsId, Guid commentId)
{
await CommandBus.PublishAsync(new DeleteComment { CommentsId = commentsId, CommentId = commentId });
return NoContent();
}
}
}

53
src/Squidex/Areas/Api/Controllers/Comments/Models/CommentDto.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using NodaTime;
using Squidex.Domain.Apps.Core.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Comments.Models
{
public sealed class CommentDto
{
/// <summary>
/// The id of the comment.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The time when the comment was created or updated last.
/// </summary>
[Required]
public Instant Time { get; set; }
/// <summary>
/// The user who created or updated the comment.
/// </summary>
[Required]
public RefToken User { get; set; }
/// <summary>
/// The text of the comment.
/// </summary>
[Required]
public string Text { get; set; }
public static CommentDto FromComment(Comment comment)
{
return SimpleMapper.Map(comment, new CommentDto());
}
public static CommentDto FromCommand(CreateComment command)
{
return SimpleMapper.Map(command, new CommentDto { User = command.Actor, Time = SystemClock.Instance.GetCurrentInstant() });
}
}
}

48
src/Squidex/Areas/Api/Controllers/Comments/Models/CommentsDto.cs

@ -0,0 +1,48 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Entities.Comments;
namespace Squidex.Areas.Api.Controllers.Comments.Models
{
public sealed class CommentsDto
{
/// <summary>
/// The created comments including the updates.
/// </summary>
public List<CommentDto> CreatedComments { get; set; }
/// <summary>
/// The updates comments since the last version.
/// </summary>
public List<CommentDto> UpdatedComments { get; set; }
/// <summary>
/// The deleted comments since the last version.
/// </summary>
public List<Guid> DeletedComments { get; set; }
/// <summary>
/// The current version.
/// </summary>
public long Version { get; set; }
public static CommentsDto FromResult(CommentsResult result)
{
return new CommentsDto
{
CreatedComments = result.CreatedComments.Select(CommentDto.FromComment).ToList(),
UpdatedComments = result.UpdatedComments.Select(CommentDto.FromComment).ToList(),
DeletedComments = result.DeletedComments,
Version = result.Version
};
}
}
}

33
src/Squidex/Areas/Api/Controllers/Comments/Models/UpsertCommentDto.cs

@ -0,0 +1,33 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Comments.Models
{
public sealed class UpsertCommentDto
{
/// <summary>
/// The comment text.
/// </summary>
[Required]
public string Text { get; set; }
public CreateComment ToCreateCommand(Guid commentsId)
{
return SimpleMapper.Map(this, new CreateComment { CommentsId = commentsId });
}
public UpdateComment ToUpdateComment(Guid commentsId, Guid commentId)
{
return SimpleMapper.Map(this, new UpdateComment { CommentsId = commentsId, CommentId = commentId });
}
}
}

5
src/Squidex/Config/Domain/EntitiesServices.cs

@ -24,6 +24,8 @@ using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Edm;
@ -160,6 +162,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<GrainCommandMiddleware<AppCommand, IAppGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<ContentCommand, IContentGrain>>()
.As<ICommandMiddleware>();

2
src/Squidex/app/shared/module.ts

@ -34,6 +34,7 @@ import {
BackupsService,
BackupsState,
ClientsState,
CommentsService,
ContentMustExistGuard,
ContentsService,
ContentsState,
@ -163,6 +164,7 @@ export class SqxSharedModule {
BackupsService,
BackupsState,
ClientsState,
CommentsService,
ContentMustExistGuard,
ContentsService,
ContentsState,

2
src/Squidex/app/shared/services/assets.service.ts

@ -237,7 +237,7 @@ export class AssetsService {
throw 'Invalid';
}
}),
tap(dto => {
tap(() => {
this.analytics.trackEvent('Asset', 'Uploaded', appName);
}),
pretifyError('Failed to upload asset. Please reload.'));

133
src/Squidex/app/shared/services/comments.service.spec.ts

@ -0,0 +1,133 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import {
ApiUrlConfig,
CommentDto,
CommentsDto,
CommentsService,
DateTime,
UpsertCommentDto,
Version
} from './../';
describe('CommentsService', () => {
const user = 'me';
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
CommentsService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get comments',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
let comments: CommentsDto;
commentsService.getComments('my-comments', new Version('123')).subscribe(result => {
comments = result;
});
const req = httpMock.expectOne('http://service/p/api/comments/my-comments');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('X-Since')).toBe('123');
req.flush({
createdComments: [{
id: '123',
text: 'text1',
time: '2016-10-12T10:10',
user: user
}],
updatedComments: [{
id: '456',
text: 'text2',
time: '2017-11-12T12:12',
user: user
}],
deletedComments: ['789'],
version: '9'
});
expect(comments!).toEqual(
new CommentsDto(
[
new CommentDto('123', DateTime.parseISO_UTC('2016-12-12T10:10'), 'text1', user)
], [
new CommentDto('456', DateTime.parseISO_UTC('2017-11-12T12:12'), 'text2', user)
], [
'789'
],
new Version('9'))
);
}));
it('should make post request to create comment',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
let comment: CommentDto;
commentsService.postComment('my-comments', new UpsertCommentDto('text1')).subscribe(result => {
comment = <CommentDto>result;
});
const req = httpMock.expectOne('http://service/p/api/comments/my-comments');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
id: '456',
text: 'text2',
time: '2017-11-12T12:12',
user: user
});
expect(comment!).toEqual(new CommentDto('123', DateTime.parseISO_UTC('2016-12-12T10:10'), 'text1', user));
}));
it('should make put request to replace comment content',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
commentsService.putComment('my-comments', '123', new UpsertCommentDto('text1')).subscribe();
const req = httpMock.expectOne('http://service/p/api/comments/my-comments/123');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make delete request to delete comment',
inject([CommentsService, HttpTestingController], (commentsService: CommentsService, httpMock: HttpTestingController) => {
commentsService.deleteComment('my-comments', '123').subscribe();
const req = httpMock.expectOne('http://service/p/api/comments/my-comments/123');
expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
});

125
src/Squidex/app/shared/services/comments.service.ts

@ -0,0 +1,125 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import {
ApiUrlConfig,
DateTime,
Model,
pretifyError,
Version
} from '@app/framework';
export class CommentsDto extends Model {
constructor(
public readonly createdComments: CommentDto[],
public readonly updatedComments: CommentDto[],
public readonly deletedComments: string[],
public readonly version: Version
) {
super();
}
}
export class CommentDto extends Model {
constructor(
public readonly id: string,
public readonly time: DateTime,
public readonly text: string,
public readonly user: string
) {
super();
}
}
export class UpsertCommentDto {
constructor(
public readonly text: string
) {
}
}
@Injectable()
export class CommentsService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getComments(commentsId: string, version: Version): Observable<CommentsDto> {
const url = this.apiUrl.buildUrl(`api/comments/${commentsId}`);
return this.http.get(url, { headers: { 'Since': version.value } }).pipe(
map(response => {
const body: any = response;
return new CommentsDto(
body.createdComments.map((item: any) => {
return new CommentDto(
item.id,
DateTime.parseISO_UTC(item.time),
item.text,
item.user);
}),
body.updatedComments.map((item: any) => {
return new CommentDto(
item.id,
DateTime.parseISO_UTC(item.time),
item.text,
item.user);
}),
body.deletedComments,
new Version(body.version)
);
}),
pretifyError('Failed to load comments.'));
}
public postComment(commentsId: string, dto: UpsertCommentDto): Observable<CommentDto> {
const url = this.apiUrl.buildUrl(`api/comments/${commentsId}`);
return this.http.post(url, dto).pipe(
map(response => {
const body: any = response;
return new CommentDto(
body.id,
DateTime.parseISO_UTC(body.time),
body.text,
body.user);
}),
pretifyError('Failed to create comment.'));
}
public putComment(commentsId: string, commentId: string, dto: UpsertCommentDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/comments/${commentsId}/${commentId}`);
return this.http.put(url, dto).pipe(
map(response => {
const body: any = response;
return new CommentDto(
body.id,
DateTime.parseISO_UTC(body.time),
body.text,
body.user);
}),
pretifyError('Failed to update comment.'));
}
public deleteComment(commentsId: string, commentId: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/comments/${commentsId}/${commentId}`);
return this.http.delete(url).pipe(
pretifyError('Failed to delete comment.'));
}
}

162
tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsGrainTests.cs

@ -0,0 +1,162 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
using NodaTime;
using Squidex.Domain.Apps.Core.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.Comments.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Comments
{
public class CommentsGrainTests : HandlerTestBase<CommentsGrain, CommentsState>
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly Guid commentsId = Guid.NewGuid();
private readonly CommentsGrain sut;
protected override Guid Id
{
get { return commentsId; }
}
public CommentsGrainTests()
{
sut = new CommentsGrain(Store, A.Dummy<ISemanticLog>());
sut.OnActivateAsync(Id).Wait();
}
[Fact]
public async Task Create_should_create_events()
{
var command = new CreateComment { Text = "text1" };
var result = await sut.ExecuteAsync(CreateCommentsCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(command.CommentId, 0));
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult { Version = 0 });
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult
{
CreatedComments = new List<Comment>
{
new Comment(command.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), command.Actor, "text1")
},
Version = 0
});
LastEvents
.ShouldHaveSameEvents(
CreateCommentsEvent(new CommentCreated { CommentId = command.CommentId, Text = command.Text })
);
}
[Fact]
public async Task Update_should_create_events_and_update_state()
{
var createCommand = new CreateComment { Text = "text1" };
var updateCommand = new UpdateComment { Text = "text2", CommentId = createCommand.CommentId };
await sut.ExecuteAsync(CreateCommentsCommand(createCommand));
var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand));
result.ShouldBeEquivalent(new EntitySavedResult(1));
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult
{
CreatedComments = new List<Comment>
{
new Comment(createCommand.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), createCommand.Actor, "text2")
},
Version = 1
});
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult
{
UpdatedComments = new List<Comment>
{
new Comment(createCommand.CommentId, LastEvents.ElementAt(0).Headers.Timestamp(), createCommand.Actor, "text2")
},
Version = 1
});
LastEvents
.ShouldHaveSameEvents(
CreateCommentsEvent(new CommentUpdated { CommentId = createCommand.CommentId, Text = updateCommand.Text })
);
}
[Fact]
public async Task Delete_should_create_events_and_update_state()
{
var createCommand = new CreateComment { Text = "text1" };
var updateCommand = new UpdateComment { Text = "text2", CommentId = createCommand.CommentId };
var deleteCommand = new DeleteComment { CommentId = createCommand.CommentId };
await sut.ExecuteAsync(CreateCommentsCommand(createCommand));
await sut.ExecuteAsync(CreateCommentsCommand(updateCommand));
var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand));
result.ShouldBeEquivalent(new EntitySavedResult(2));
sut.GetCommentsAsync(-1).Result.Should().BeEquivalentTo(new CommentsResult { Version = 2 });
sut.GetCommentsAsync(0).Result.Should().BeEquivalentTo(new CommentsResult
{
DeletedComments = new List<Guid>
{
deleteCommand.CommentId
},
Version = 2
});
sut.GetCommentsAsync(1).Result.Should().BeEquivalentTo(new CommentsResult
{
DeletedComments = new List<Guid>
{
deleteCommand.CommentId
},
Version = 2
});
LastEvents
.ShouldHaveSameEvents(
CreateCommentsEvent(new CommentDeleted { CommentId = createCommand.CommentId })
);
}
private Task ExecuteDeleteAsync()
{
return sut.ExecuteAsync(CreateCommentsCommand(new DeleteComment()));
}
protected T CreateCommentsEvent<T>(T @event) where T : CommentsEvent
{
@event.CommentsId = commentsId;
return CreateEvent(@event);
}
protected T CreateCommentsCommand<T>(T command) where T : CommentsCommand
{
command.CommentsId = commentsId;
return CreateCommand(command);
}
}
}

164
tests/Squidex.Domain.Apps.Entities.Tests/Comments/Guards/GuardCommentsTests.cs

@ -0,0 +1,164 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Comments;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Comments.Guards
{
public class GuardCommentsTests
{
private readonly RefToken user1 = new RefToken(RefTokenType.Subject, "1");
private readonly RefToken user2 = new RefToken(RefTokenType.Subject, "2");
[Fact]
public void CanCreate_should_throw_exception_if_text_not_defined()
{
var command = new CreateComment();
ValidationAssert.Throws(() => GuardComments.CanCreate(command),
new ValidationError("Text is required.", "Text"));
}
[Fact]
public void CanCreate_should_not_throw_exception_if_text_defined()
{
var command = new CreateComment { Text = "text" };
GuardComments.CanCreate(command);
}
[Fact]
public void CanUpdate_should_throw_exception_if_text_not_defined()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
ValidationAssert.Throws(() => GuardComments.CanUpdate(events, command),
new ValidationError("Text is required.", "Text"));
}
[Fact]
public void CanUpdate_should_throw_exception_if_comment_from_another_user()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user2, Text = "text2" };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
Assert.Throws<DomainException>(() => GuardComments.CanUpdate(events, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_comment_not_found()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>();
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(events, command));
}
[Fact]
public void CanUpdate_should_throw_exception_if_comment_deleted_found()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(),
Envelope.Create(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>(),
};
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanUpdate(events, command));
}
[Fact]
public void CanUpdate_should_not_throw_exception_if_comment_from_same_user()
{
var commentId = Guid.NewGuid();
var command = new UpdateComment { CommentId = commentId, Actor = user1, Text = "text2" };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
GuardComments.CanUpdate(events, command);
}
[Fact]
public void CanDelete_should_throw_exception_if_comment_from_another_user()
{
var commentId = Guid.NewGuid();
var command = new DeleteComment { CommentId = commentId, Actor = user2 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
Assert.Throws<DomainException>(() => GuardComments.CanDelete(events, command));
}
[Fact]
public void CanDelete_should_throw_exception_if_comment_not_found()
{
var commentId = Guid.NewGuid();
var command = new DeleteComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>();
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(events, command));
}
[Fact]
public void CanDelete_should_throw_exception_if_comment_deleted()
{
var commentId = Guid.NewGuid();
var command = new DeleteComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>(),
Envelope.Create(new CommentDeleted { CommentId = commentId }).To<CommentsEvent>(),
};
Assert.Throws<DomainObjectNotFoundException>(() => GuardComments.CanDelete(events, command));
}
[Fact]
public void CanDelete_should_not_throw_exception_if_comment_from_same_user()
{
var commentId = Guid.NewGuid();
var command = new DeleteComment { CommentId = commentId, Actor = user1 };
var events = new List<Envelope<CommentsEvent>>
{
Envelope.Create(new CommentCreated { CommentId = commentId, Actor = user1 }).To<CommentsEvent>()
};
GuardComments.CanDelete(events, command);
}
}
}
Loading…
Cancel
Save