-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented content locale creation/replacement. (#21)
- Loading branch information
Showing
9 changed files
with
363 additions
and
0 deletions.
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
backend/src/Logitar.Cms.Contracts/Contents/SaveContentLocalePayload.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
namespace Logitar.Cms.Contracts.Contents; | ||
|
||
public class SaveContentLocalePayload | ||
{ | ||
public string UniqueName { get; set; } | ||
|
||
public SaveContentLocalePayload() : this(string.Empty) | ||
{ | ||
} | ||
|
||
public SaveContentLocalePayload(string uniqueName) | ||
{ | ||
UniqueName = uniqueName; | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
backend/src/Logitar.Cms.Core/Contents/CannotCreateInvariantLocaleException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using Logitar.Cms.Contracts.Errors; | ||
|
||
namespace Logitar.Cms.Core.Contents; | ||
|
||
public class CannotCreateInvariantLocaleException : BadRequestException | ||
{ | ||
private const string ErrorMessage = "A locale cannot be created for an invariant content."; | ||
|
||
public string ContentId | ||
{ | ||
get => (string)Data[nameof(ContentId)]!; | ||
private set => Data[nameof(ContentId)] = value; | ||
} | ||
public string ContentTypeId | ||
{ | ||
get => (string)Data[nameof(ContentTypeId)]!; | ||
private set => Data[nameof(ContentTypeId)] = value; | ||
} | ||
|
||
public override Error Error => new(this.GetErrorCode(), ErrorMessage); | ||
|
||
public CannotCreateInvariantLocaleException(ContentAggregate content) | ||
: base(BuildMessage(content)) | ||
{ | ||
ContentId = content.Id.Value; | ||
ContentTypeId = content.ContentTypeId.Value; | ||
} | ||
|
||
private static string BuildMessage(ContentAggregate content) => new ErrorMessageBuilder(ErrorMessage) | ||
.AddData(nameof(ContentId), content.Id.Value) | ||
.AddData(nameof(ContentTypeId), content.ContentTypeId.Value) | ||
.Build(); | ||
} |
6 changes: 6 additions & 0 deletions
6
backend/src/Logitar.Cms.Core/Contents/Commands/SaveContentLocaleCommand.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
using Logitar.Cms.Contracts.Contents; | ||
using MediatR; | ||
|
||
namespace Logitar.Cms.Core.Contents.Commands; | ||
|
||
public record SaveContentLocaleCommand(Guid ContentId, Guid? LanguageId, SaveContentLocalePayload Payload) : Activity, IRequest<ContentItem?>; |
74 changes: 74 additions & 0 deletions
74
backend/src/Logitar.Cms.Core/Contents/Commands/SaveContentLocaleCommandHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
using FluentValidation; | ||
using Logitar.Cms.Contracts.Contents; | ||
using Logitar.Cms.Core.Contents.Validators; | ||
using Logitar.Cms.Core.ContentTypes; | ||
using Logitar.Cms.Core.Languages; | ||
using Logitar.Identity.Contracts.Settings; | ||
using Logitar.Identity.Domain.Shared; | ||
using MediatR; | ||
|
||
namespace Logitar.Cms.Core.Contents.Commands; | ||
|
||
internal class SaveContentLocaleCommandHandler : IRequestHandler<SaveContentLocaleCommand, ContentItem?> | ||
{ | ||
private readonly IContentQuerier _contentQuerier; | ||
private readonly IContentRepository _contentRepository; | ||
private readonly IContentTypeRepository _contentTypeRepository; | ||
private readonly ILanguageRepository _languageRepository; | ||
private readonly ISender _sender; | ||
|
||
public SaveContentLocaleCommandHandler( | ||
IContentQuerier contentQuerier, | ||
IContentRepository contentRepository, | ||
IContentTypeRepository contentTypeRepository, | ||
ILanguageRepository languageRepository, | ||
ISender sender) | ||
{ | ||
_contentQuerier = contentQuerier; | ||
_contentRepository = contentRepository; | ||
_contentTypeRepository = contentTypeRepository; | ||
_languageRepository = languageRepository; | ||
_sender = sender; | ||
} | ||
|
||
public async Task<ContentItem?> Handle(SaveContentLocaleCommand command, CancellationToken cancellationToken) | ||
{ | ||
IUniqueNameSettings uniqueNameSettings = ContentAggregate.UniqueNameSettings; | ||
|
||
SaveContentLocalePayload payload = command.Payload; | ||
new SaveContentLocaleValidator(uniqueNameSettings).ValidateAndThrow(payload); | ||
|
||
ContentId contentId = new(command.ContentId); | ||
ContentAggregate? content = await _contentRepository.LoadAsync(contentId, cancellationToken); | ||
if (content == null) | ||
{ | ||
return null; | ||
} | ||
|
||
ContentLocaleUnit locale = new(new UniqueNameUnit(uniqueNameSettings, payload.UniqueName)); | ||
|
||
if (command.LanguageId.HasValue) | ||
{ | ||
ContentTypeAggregate contentType = await _contentTypeRepository.LoadAsync(content.ContentTypeId, cancellationToken) | ||
?? throw new InvalidOperationException($"The content type aggregate 'Id={content.ContentTypeId.Value}' could not be found."); | ||
if (contentType.IsInvariant) | ||
{ | ||
throw new CannotCreateInvariantLocaleException(content); | ||
} | ||
|
||
LanguageId languageId = new(command.LanguageId.Value); | ||
LanguageAggregate language = await _languageRepository.LoadAsync(languageId, cancellationToken) | ||
?? throw new AggregateNotFoundException<LanguageAggregate>(languageId.AggregateId, nameof(command.LanguageId)); | ||
|
||
content.SetLocale(language, locale, command.ActorId); | ||
} | ||
else | ||
{ | ||
content.SetInvariant(locale, command.ActorId); | ||
} | ||
|
||
await _sender.Send(new SaveContentCommand(content), cancellationToken); | ||
|
||
return await _contentQuerier.ReadAsync(content, cancellationToken); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
backend/src/Logitar.Cms.Core/Contents/Validators/SaveContentLocaleValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using FluentValidation; | ||
using Logitar.Cms.Contracts.Contents; | ||
using Logitar.Identity.Contracts.Settings; | ||
using Logitar.Identity.Domain.Shared; | ||
|
||
namespace Logitar.Cms.Core.Contents.Validators; | ||
|
||
public class SaveContentLocaleValidator : AbstractValidator<SaveContentLocalePayload> | ||
{ | ||
public SaveContentLocaleValidator(IUniqueNameSettings uniqueNameSettings) | ||
{ | ||
RuleFor(x => x.UniqueName).SetValidator(new UniqueNameValidator(uniqueNameSettings)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
...gitar.Cms.Core.IntegrationTests/Contents/Commands/SaveContentLocaleCommandHandlerTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
using Logitar.Cms.Contracts.Contents; | ||
using Logitar.Cms.Core.ContentTypes; | ||
using Logitar.Cms.Core.Languages; | ||
using Logitar.Identity.Domain.Shared; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Logitar.Cms.Core.Contents.Commands; | ||
|
||
[Trait(Traits.Category, Categories.Integration)] | ||
public class SaveContentLocaleCommandHandlerTests : IntegrationTests | ||
{ | ||
private readonly IContentRepository _contentRepository; | ||
private readonly IContentTypeRepository _contentTypeRepository; | ||
private readonly ILanguageRepository _languageRepository; | ||
|
||
private readonly ContentTypeAggregate _contentType; | ||
private readonly ContentAggregate _content; | ||
|
||
public SaveContentLocaleCommandHandlerTests() : base() | ||
{ | ||
_contentRepository = ServiceProvider.GetRequiredService<IContentRepository>(); | ||
_contentTypeRepository = ServiceProvider.GetRequiredService<IContentTypeRepository>(); | ||
_languageRepository = ServiceProvider.GetRequiredService<ILanguageRepository>(); | ||
|
||
_contentType = new(new IdentifierUnit("BlogArticle"), isInvariant: false, ActorId); | ||
_content = new(_contentType, new ContentLocaleUnit(new UniqueNameUnit(ContentAggregate.UniqueNameSettings, "rendered-lego-acura-models")), ActorId); | ||
} | ||
|
||
public override async Task InitializeAsync() | ||
{ | ||
await base.InitializeAsync(); | ||
|
||
await _contentTypeRepository.SaveAsync(_contentType); | ||
await _contentRepository.SaveAsync(_content); | ||
} | ||
|
||
[Fact(DisplayName = "It should save a content locale.")] | ||
public async Task It_should_save_a_content_locale() | ||
{ | ||
LanguageAggregate language = Assert.Single(await _languageRepository.LoadAsync()); | ||
|
||
SaveContentLocalePayload payload = new("rendered-lego-acura-models-2"); | ||
SaveContentLocaleCommand command = new(_content.Id.ToGuid(), language.Id.ToGuid(), payload); | ||
ContentItem? content = await Pipeline.ExecuteAsync(command); | ||
Assert.NotNull(content); | ||
|
||
Assert.Equal(_content.Id.ToGuid(), content.Id); | ||
Assert.Equal(_content.Version + 1, content.Version); | ||
Assert.Equal(Actor, content.UpdatedBy); | ||
Assert.True(content.CreatedOn < content.UpdatedOn); | ||
|
||
ContentLocale locale = Assert.Single(content.Locales, l => l.Language != null); | ||
Assert.NotEqual(default, locale.Id); | ||
Assert.Same(content, locale.Item); | ||
Assert.NotNull(locale.Language); | ||
Assert.Equal(language.Id.ToGuid(), locale.Language.Id); | ||
Assert.Equal(payload.UniqueName, locale.UniqueName); | ||
Assert.Equal(Actor, locale.CreatedBy); | ||
Assert.NotEqual(default, locale.CreatedOn); | ||
Assert.Equal(Actor, locale.UpdatedBy); | ||
Assert.Equal(locale.CreatedOn, locale.UpdatedOn); | ||
} | ||
} |
143 changes: 143 additions & 0 deletions
143
...ests/Logitar.Cms.Core.UnitTests/Contents/Commands/SaveContentLocaleCommandHandlerTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
using FluentValidation.Results; | ||
using Logitar.Cms.Contracts.Contents; | ||
using Logitar.Cms.Core.ContentTypes; | ||
using Logitar.Cms.Core.Languages; | ||
using Logitar.Identity.Domain.Shared; | ||
using MediatR; | ||
using Moq; | ||
|
||
namespace Logitar.Cms.Core.Contents.Commands; | ||
|
||
[Trait(Traits.Category, Categories.Unit)] | ||
public class SaveContentLocaleCommandHandlerTests | ||
{ | ||
private readonly CancellationToken _cancellationToken = default; | ||
|
||
private readonly Mock<IContentQuerier> _contentQuerier = new(); | ||
private readonly Mock<IContentRepository> _contentRepository = new(); | ||
private readonly Mock<IContentTypeRepository> _contentTypeRepository = new(); | ||
private readonly Mock<ILanguageRepository> _languageRepository = new(); | ||
private readonly Mock<ISender> _sender = new(); | ||
|
||
private readonly SaveContentLocaleCommandHandler _handler; | ||
|
||
private readonly ContentTypeAggregate _articleType; | ||
private readonly ContentTypeAggregate _authorType; | ||
private readonly ContentAggregate _article; | ||
private readonly ContentAggregate _author; | ||
private readonly LanguageAggregate _english; | ||
|
||
public SaveContentLocaleCommandHandlerTests() | ||
{ | ||
_handler = new(_contentQuerier.Object, _contentRepository.Object, _contentTypeRepository.Object, _languageRepository.Object, _sender.Object); | ||
|
||
_articleType = new(new IdentifierUnit("BlogArticle"), isInvariant: false); | ||
_authorType = new(new IdentifierUnit("BlogAuthor"), isInvariant: true); | ||
_article = new(_articleType, new ContentLocaleUnit(new UniqueNameUnit(ContentAggregate.UniqueNameSettings, "rendered-lego-acura-models"))); | ||
_author = new(_authorType, new ContentLocaleUnit(new UniqueNameUnit(ContentAggregate.UniqueNameSettings, "ryan-hucks"))); | ||
_english = new(new LocaleUnit("en"), isDefault: true); | ||
} | ||
|
||
[Fact(DisplayName = "It should create a new locale.")] | ||
public async Task It_should_create_a_new_locale() | ||
{ | ||
_contentRepository.Setup(x => x.LoadAsync(_article.Id, _cancellationToken)).ReturnsAsync(_article); | ||
_contentTypeRepository.Setup(x => x.LoadAsync(_articleType.Id, _cancellationToken)).ReturnsAsync(_articleType); | ||
_languageRepository.Setup(x => x.LoadAsync(_english.Id, _cancellationToken)).ReturnsAsync(_english); | ||
|
||
SaveContentLocalePayload payload = new("rendered-lego-acura-models"); | ||
SaveContentLocaleCommand command = new(_article.Id.ToGuid(), _english.Id.ToGuid(), payload); | ||
ActivityHelper.Contextualize(command); | ||
|
||
await _handler.Handle(command, _cancellationToken); | ||
|
||
_sender.Verify(x => x.Send(It.Is<SaveContentCommand>(y => y.Content.Equals(_article)), _cancellationToken), Times.Once); | ||
|
||
ContentLocaleUnit? locale = _article.TryGetLocale(_english); | ||
Assert.NotNull(locale); | ||
Assert.Equal(payload.UniqueName, locale.UniqueName.Value); | ||
} | ||
|
||
[Fact(DisplayName = "It should return null when the content cannot be found.")] | ||
public async Task It_should_return_null_when_the_content_cannot_be_found() | ||
{ | ||
SaveContentLocalePayload payload = new("rendered-lego-acura-models"); | ||
SaveContentLocaleCommand command = new(ContentId: Guid.NewGuid(), LanguageId: null, payload); | ||
Assert.Null(await _handler.Handle(command, _cancellationToken)); | ||
} | ||
|
||
[Fact(DisplayName = "It should replace an existing locale.")] | ||
public async Task It_should_replace_an_existing_locale() | ||
{ | ||
_article.SetLocale(_english, _article.Invariant); | ||
|
||
_contentRepository.Setup(x => x.LoadAsync(_article.Id, _cancellationToken)).ReturnsAsync(_article); | ||
_contentTypeRepository.Setup(x => x.LoadAsync(_articleType.Id, _cancellationToken)).ReturnsAsync(_articleType); | ||
_languageRepository.Setup(x => x.LoadAsync(_english.Id, _cancellationToken)).ReturnsAsync(_english); | ||
|
||
SaveContentLocalePayload payload = new("rendered-lego-acura-models-2"); | ||
SaveContentLocaleCommand command = new(_article.Id.ToGuid(), _english.Id.ToGuid(), payload); | ||
ActivityHelper.Contextualize(command); | ||
|
||
await _handler.Handle(command, _cancellationToken); | ||
|
||
_sender.Verify(x => x.Send(It.Is<SaveContentCommand>(y => y.Content.Equals(_article)), _cancellationToken), Times.Once); | ||
|
||
ContentLocaleUnit? locale = _article.TryGetLocale(_english); | ||
Assert.NotNull(locale); | ||
Assert.Equal(payload.UniqueName, locale.UniqueName.Value); | ||
} | ||
|
||
[Fact(DisplayName = "It should replace the content invariant.")] | ||
public async Task It_should_replace_the_content_invariant() | ||
{ | ||
_contentRepository.Setup(x => x.LoadAsync(_article.Id, _cancellationToken)).ReturnsAsync(_article); | ||
|
||
SaveContentLocalePayload payload = new("rendered-lego-acura-models-2"); | ||
SaveContentLocaleCommand command = new(_article.Id.ToGuid(), LanguageId: null, payload); | ||
ActivityHelper.Contextualize(command); | ||
|
||
await _handler.Handle(command, _cancellationToken); | ||
|
||
_sender.Verify(x => x.Send(It.Is<SaveContentCommand>(y => y.Content.Equals(_article)), _cancellationToken), Times.Once); | ||
|
||
Assert.Equal(payload.UniqueName, _article.Invariant.UniqueName.Value); | ||
} | ||
|
||
[Fact(DisplayName = "It should throw AggregateNotFoundException when the language cannot be found.")] | ||
public async Task It_should_throw_AggregateNotFoundException_when_the_language_cannot_be_found() | ||
{ | ||
_contentRepository.Setup(x => x.LoadAsync(_article.Id, _cancellationToken)).ReturnsAsync(_article); | ||
_contentTypeRepository.Setup(x => x.LoadAsync(_articleType.Id, _cancellationToken)).ReturnsAsync(_articleType); | ||
|
||
SaveContentLocalePayload payload = new("rendered-lego-acura-models"); | ||
SaveContentLocaleCommand command = new(_article.Id.ToGuid(), _english.Id.ToGuid(), payload); | ||
var exception = await Assert.ThrowsAsync<AggregateNotFoundException<LanguageAggregate>>(async () => await _handler.Handle(command, _cancellationToken)); | ||
Assert.Equal(_english.Id.Value, exception.Id); | ||
Assert.Equal("LanguageId", exception.PropertyName); | ||
} | ||
|
||
[Fact(DisplayName = "It should throw CannotCreateInvariantLocaleException when creating a locale to an invariant content.")] | ||
public async Task It_should_throw_CannotCreateInvariantLocaleException_when_creating_a_locale_to_an_invariant_content() | ||
{ | ||
_contentRepository.Setup(x => x.LoadAsync(_author.Id, _cancellationToken)).ReturnsAsync(_author); | ||
_contentTypeRepository.Setup(x => x.LoadAsync(_authorType.Id, _cancellationToken)).ReturnsAsync(_authorType); | ||
|
||
SaveContentLocalePayload payload = new("ryan-hucks"); | ||
SaveContentLocaleCommand command = new(_author.Id.ToGuid(), _english.Id.ToGuid(), payload); | ||
var exception = await Assert.ThrowsAsync<CannotCreateInvariantLocaleException>(async () => await _handler.Handle(command, _cancellationToken)); | ||
Assert.Equal(_author.Id.Value, exception.ContentId); | ||
Assert.Equal(_authorType.Id.Value, exception.ContentTypeId); | ||
} | ||
|
||
[Fact(DisplayName = "It should throw ValidationException when the payload is not valid.")] | ||
public async Task It_should_throw_ValidationException_when_the_payload_is_not_valid() | ||
{ | ||
SaveContentLocalePayload payload = new("rendered-lego-acura-models!"); | ||
SaveContentLocaleCommand command = new(ContentId: Guid.NewGuid(), LanguageId: null, payload); | ||
var exception = await Assert.ThrowsAsync<FluentValidation.ValidationException>(async () => await _handler.Handle(command, _cancellationToken)); | ||
ValidationFailure error = Assert.Single(exception.Errors); | ||
Assert.Equal("AllowedCharactersValidator", error.ErrorCode); | ||
Assert.Equal("UniqueName", error.PropertyName); | ||
} | ||
} |