From 2c7e4dd30d1f1f36058544503c49411bae6d61c4 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Mon, 22 Jul 2024 12:48:27 -0400 Subject: [PATCH] Implemented field type update. --- .../Commands/UpdateFieldTypeCommandHandler.cs | 77 ++++++++++++++++++ .../Validators/UpdateFieldTypeValidator.cs | 57 +++++++++++++ .../Controllers/FieldTypeController.cs | 7 ++ .../UpdateFieldTypeCommandHandlerTests.cs | 65 +++++++++++++++ .../ReplaceFieldTypeCommandHandlerTests.cs | 20 ++--- .../UpdateFieldTypeCommandHandlerTests.cs | 80 +++++++++++++++++++ 6 files changed, 296 insertions(+), 10 deletions(-) create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommandHandler.cs create mode 100644 backend/src/Logitar.Cms.Core/FieldTypes/Validators/UpdateFieldTypeValidator.cs create mode 100644 backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs create mode 100644 backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommandHandler.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommandHandler.cs new file mode 100644 index 0000000..f13b8cd --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Commands/UpdateFieldTypeCommandHandler.cs @@ -0,0 +1,77 @@ +using FluentValidation; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Cms.Core.FieldTypes.Validators; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Shared; +using MediatR; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +internal class UpdateFieldTypeCommandHandler : IRequestHandler +{ + private readonly IFieldTypeQuerier _fieldTypeQuerier; + private readonly IFieldTypeRepository _fieldTypeRepository; + private readonly ISender _sender; + + public UpdateFieldTypeCommandHandler(IFieldTypeQuerier fieldTypeQuerier, IFieldTypeRepository fieldTypeRepository, ISender sender) + { + _fieldTypeQuerier = fieldTypeQuerier; + _fieldTypeRepository = fieldTypeRepository; + _sender = sender; + } + + public async Task Handle(UpdateFieldTypeCommand command, CancellationToken cancellationToken) + { + FieldTypeId id = new(command.Id); + FieldTypeAggregate? fieldType = await _fieldTypeRepository.LoadAsync(id, cancellationToken); + if (fieldType == null) + { + return null; + } + + IUniqueNameSettings uniqueNameSettings = FieldTypeAggregate.UniqueNameSettings; + + UpdateFieldTypePayload payload = command.Payload; + new UpdateFieldTypeValidator(uniqueNameSettings, fieldType.DataType).ValidateAndThrow(payload); + + if (!string.IsNullOrWhiteSpace(payload.UniqueName)) + { + fieldType.UniqueName = new UniqueNameUnit(uniqueNameSettings, payload.UniqueName); + } + if (payload.DisplayName != null) + { + fieldType.DisplayName = DisplayNameUnit.TryCreate(payload.DisplayName.Value); + } + if (payload.Description != null) + { + fieldType.Description = DescriptionUnit.TryCreate(payload.Description.Value); + } + fieldType.Update(command.ActorId); + + if (payload.BooleanProperties != null) + { + fieldType.SetProperties(new ReadOnlyBooleanProperties(payload.BooleanProperties), command.ActorId); + } + if (payload.DateTimeProperties != null) + { + fieldType.SetProperties(new ReadOnlyDateTimeProperties(payload.DateTimeProperties), command.ActorId); + } + if (payload.NumberProperties != null) + { + fieldType.SetProperties(new ReadOnlyNumberProperties(payload.NumberProperties), command.ActorId); + } + if (payload.StringProperties != null) + { + fieldType.SetProperties(new ReadOnlyStringProperties(payload.StringProperties), command.ActorId); + } + if (payload.TextProperties != null) + { + fieldType.SetProperties(new ReadOnlyTextProperties(payload.TextProperties), command.ActorId); + } + + await _sender.Send(new SaveFieldTypeCommand(fieldType), cancellationToken); + + return await _fieldTypeQuerier.ReadAsync(fieldType, cancellationToken); + } +} diff --git a/backend/src/Logitar.Cms.Core/FieldTypes/Validators/UpdateFieldTypeValidator.cs b/backend/src/Logitar.Cms.Core/FieldTypes/Validators/UpdateFieldTypeValidator.cs new file mode 100644 index 0000000..bfa3aab --- /dev/null +++ b/backend/src/Logitar.Cms.Core/FieldTypes/Validators/UpdateFieldTypeValidator.cs @@ -0,0 +1,57 @@ +using FluentValidation; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Identity.Contracts.Settings; +using Logitar.Identity.Domain.Shared; + +namespace Logitar.Cms.Core.FieldTypes.Validators; + +public class UpdateFieldTypeValidator : AbstractValidator +{ + public UpdateFieldTypeValidator(IUniqueNameSettings uniqueNameSettings, DataType dataType) + { + When(x => !string.IsNullOrWhiteSpace(x.UniqueName), () => RuleFor(x => x.UniqueName!).SetValidator(new UniqueNameValidator(uniqueNameSettings))); + When(x => !string.IsNullOrWhiteSpace(x.DisplayName?.Value), () => RuleFor(x => x.DisplayName!.Value!).SetValidator(new DisplayNameValidator())); + When(x => !string.IsNullOrWhiteSpace(x.Description?.Value), () => RuleFor(x => x.Description!.Value!).SetValidator(new DescriptionValidator())); + + switch (dataType) + { + case DataType.Boolean: + When(x => x.BooleanProperties != null, () => RuleFor(x => x.BooleanProperties!).SetValidator(new BooleanPropertiesValidator())); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.DateTime: + RuleFor(x => x.BooleanProperties).Null(); + When(x => x.DateTimeProperties != null, () => RuleFor(x => x.DateTimeProperties!).SetValidator(new DateTimePropertiesValidator())); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.Number: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + When(x => x.NumberProperties != null, () => RuleFor(x => x.NumberProperties!).SetValidator(new NumberPropertiesValidator())); + RuleFor(x => x.StringProperties).Null(); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.String: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + When(x => x.StringProperties != null, () => RuleFor(x => x.StringProperties!).SetValidator(new StringPropertiesValidator())); + RuleFor(x => x.TextProperties).Null(); + break; + case DataType.Text: + RuleFor(x => x.BooleanProperties).Null(); + RuleFor(x => x.DateTimeProperties).Null(); + RuleFor(x => x.NumberProperties).Null(); + RuleFor(x => x.StringProperties).Null(); + When(x => x.TextProperties != null, () => RuleFor(x => x.TextProperties!).SetValidator(new TextPropertiesValidator())); + break; + default: + throw new DataTypeNotSupportedException(dataType); + } + } +} diff --git a/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs b/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs index 273e412..8fa148f 100644 --- a/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs +++ b/backend/src/Logitar.Cms.Web/Controllers/FieldTypeController.cs @@ -57,4 +57,11 @@ public async Task>> SearchAsync([FromQuery SearchResults fieldTypes = await _pipeline.ExecuteAsync(new SearchFieldTypesQuery(parameters.ToPayload()), cancellationToken); return Ok(fieldTypes); } + + [HttpPatch("{id}")] + public async Task> UpdateAsync(Guid id, [FromBody] UpdateFieldTypePayload payload, CancellationToken cancellationToken) + { + FieldType? fieldType = await _pipeline.ExecuteAsync(new UpdateFieldTypeCommand(id, payload), cancellationToken); + return fieldType == null ? NotFound() : Ok(fieldType); + } } diff --git a/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs new file mode 100644 index 0000000..52db1bb --- /dev/null +++ b/backend/tests/Logitar.Cms.Core.IntegrationTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs @@ -0,0 +1,65 @@ +using Logitar.Cms.Contracts; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.FieldTypes.Properties; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Identity.Domain.Shared; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +[Trait(Traits.Category, Categories.Integration)] +public class UpdateFieldTypeCommandHandlerTests : IntegrationTests +{ + private readonly IFieldTypeRepository _fieldTypeRepository; + + private readonly FieldTypeAggregate _fieldType; + + public UpdateFieldTypeCommandHandlerTests() : base() + { + _fieldTypeRepository = ServiceProvider.GetRequiredService(); + + _fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "ArticleTitle"), new ReadOnlyStringProperties()); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _fieldTypeRepository.SaveAsync(_fieldType); + } + + [Fact(DisplayName = "It should update an existing field type.")] + public async Task It_should_update_an_existing_field_type() + { + UpdateFieldTypePayload payload = new() + { + DisplayName = new Change(" Article Title "), + StringProperties = new StringProperties + { + MinimumLength = 1, + MaximumLength = 100 + } + }; + UpdateFieldTypeCommand command = new(_fieldType.Id.ToGuid(), payload); + FieldType? fieldType = await Pipeline.ExecuteAsync(command); + Assert.NotNull(fieldType); + + Assert.Equal(_fieldType.Id.ToGuid(), fieldType.Id); + Assert.Equal(4, fieldType.Version); + Assert.Equal(Contracts.Actors.Actor.System, fieldType.CreatedBy); + Assert.Equal(_fieldType.CreatedOn.AsUniversalTime(), fieldType.CreatedOn); + Assert.Equal(Actor, fieldType.UpdatedBy); + Assert.True(fieldType.CreatedOn < fieldType.UpdatedOn); + + Assert.Equal(_fieldType.UniqueName.Value, fieldType.UniqueName); + Assert.NotNull(payload.DisplayName.Value); + Assert.Equal(payload.DisplayName.Value.Trim(), fieldType.DisplayName); + Assert.Null(fieldType.Description); + Assert.Equal(DataType.String, fieldType.DataType); + Assert.Null(fieldType.BooleanProperties); + Assert.Null(fieldType.DateTimeProperties); + Assert.Null(fieldType.NumberProperties); + Assert.Equal(payload.StringProperties, fieldType.StringProperties); + Assert.Null(fieldType.TextProperties); + } +} diff --git a/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs index 4f9b131..1580117 100644 --- a/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs +++ b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/ReplaceFieldTypeCommandHandlerTests.cs @@ -24,8 +24,8 @@ public ReplaceFieldTypeCommandHandlerTests() _handler = new(_fieldTypeQuerier.Object, _fieldTypeRepository.Object, _sender.Object); } - [Fact(DisplayName = "It should replace a new Boolean field type.")] - public async Task It_should_replace_a_new_Boolean_field_type() + [Fact(DisplayName = "It should replace an existing Boolean field type.")] + public async Task It_should_replace_an_existing_Boolean_field_type() { FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "Featured"), new ReadOnlyBooleanProperties()); _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); @@ -49,8 +49,8 @@ public async Task It_should_replace_a_new_Boolean_field_type() ), _cancellationToken), Times.Once()); } - [Fact(DisplayName = "It should replace a new DateTime field type.")] - public async Task It_should_replace_a_new_DateTime_field_type() + [Fact(DisplayName = "It should replace an existing DateTime field type.")] + public async Task It_should_replace_an_existing_DateTime_field_type() { FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "Published"), new ReadOnlyDateTimeProperties()); _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); @@ -74,8 +74,8 @@ public async Task It_should_replace_a_new_DateTime_field_type() ), _cancellationToken), Times.Once()); } - [Fact(DisplayName = "It should replace a new Number field type.")] - public async Task It_should_replace_a_new_Number_field_type() + [Fact(DisplayName = "It should replace an existing Number field type.")] + public async Task It_should_replace_an_existing_Number_field_type() { FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "WordCount"), new ReadOnlyNumberProperties()); _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); @@ -99,8 +99,8 @@ public async Task It_should_replace_a_new_Number_field_type() ), _cancellationToken), Times.Once()); } - [Fact(DisplayName = "It should replace a new String field type.")] - public async Task It_should_replace_a_new_String_field_type() + [Fact(DisplayName = "It should replace an existing String field type.")] + public async Task It_should_replace_an_existing_String_field_type() { FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "BlogTitle"), new ReadOnlyStringProperties()); _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); @@ -124,8 +124,8 @@ public async Task It_should_replace_a_new_String_field_type() ), _cancellationToken), Times.Once()); } - [Fact(DisplayName = "It should replace a new Text field type.")] - public async Task It_should_replace_a_new_Text_field_type() + [Fact(DisplayName = "It should replace an existing Text field type.")] + public async Task It_should_replace_an_existing_Text_field_type() { FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "BlogContent"), new ReadOnlyTextProperties()); _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); diff --git a/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs new file mode 100644 index 0000000..37f2557 --- /dev/null +++ b/backend/tests/Logitar.Cms.Core.UnitTests/FieldTypes/Commands/UpdateFieldTypeCommandHandlerTests.cs @@ -0,0 +1,80 @@ +using FluentValidation.Results; +using Logitar.Cms.Contracts; +using Logitar.Cms.Contracts.FieldTypes; +using Logitar.Cms.Contracts.FieldTypes.Properties; +using Logitar.Cms.Core.FieldTypes.Properties; +using Logitar.Identity.Domain.Shared; +using MediatR; +using Moq; + +namespace Logitar.Cms.Core.FieldTypes.Commands; + +[Trait(Traits.Category, Categories.Unit)] +public class UpdateFieldTypeCommandHandlerTests +{ + private readonly CancellationToken _cancellationToken = default; + + private readonly Mock _fieldTypeQuerier = new(); + private readonly Mock _fieldTypeRepository = new(); + private readonly Mock _sender = new(); + + private readonly UpdateFieldTypeCommandHandler _handler; + + public UpdateFieldTypeCommandHandlerTests() + { + _handler = new(_fieldTypeQuerier.Object, _fieldTypeRepository.Object, _sender.Object); + } + + [Fact(DisplayName = "It should return null when the field type is not found.")] + public async Task It_should_return_null_when_the_field_type_is_not_found() + { + UpdateFieldTypePayload payload = new(); + UpdateFieldTypeCommand command = new(Guid.Empty, payload); + Assert.Null(await _handler.Handle(command, _cancellationToken)); + } + + [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() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "ArticleTitle"), new ReadOnlyStringProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + UpdateFieldTypePayload payload = new() + { + StringProperties = new StringProperties(), + TextProperties = new TextProperties() + }; + UpdateFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload); + var exception = await Assert.ThrowsAsync(async () => await _handler.Handle(command, _cancellationToken)); + ValidationFailure error = Assert.Single(exception.Errors); + Assert.Equal("NullValidator", error.ErrorCode); + Assert.Equal("TextProperties", error.PropertyName); + } + + [Fact(DisplayName = "It should update an existing field type.")] + public async Task It_should_update_an_existing_field_type() + { + FieldTypeAggregate fieldType = new(new UniqueNameUnit(FieldTypeAggregate.UniqueNameSettings, "ArticleTitle"), new ReadOnlyStringProperties()); + _fieldTypeRepository.Setup(x => x.LoadAsync(fieldType.Id, _cancellationToken)).ReturnsAsync(fieldType); + + UpdateFieldTypePayload payload = new() + { + DisplayName = new Change(" Article Title "), + Description = new Change(" "), + StringProperties = new StringProperties(minimumLength: 1, maximumLength: 100, pattern: null) + }; + UpdateFieldTypeCommand command = new(fieldType.Id.ToGuid(), payload); + ActivityHelper.Contextualize(command); + await _handler.Handle(command, _cancellationToken); + + _sender.Verify(x => x.Send(It.Is(y => y.FieldType.Id == fieldType.Id + && y.FieldType.UniqueName == fieldType.UniqueName + && y.FieldType.DisplayName != null && payload.DisplayName.Value != null && y.FieldType.DisplayName.Value == payload.DisplayName.Value.Trim() + && y.FieldType.Description == null + && y.FieldType.DataType == DataType.String + && ((ReadOnlyStringProperties)y.FieldType.Properties).MinimumLength == payload.StringProperties.MinimumLength + && ((ReadOnlyStringProperties)y.FieldType.Properties).MaximumLength == payload.StringProperties.MaximumLength + && ((ReadOnlyStringProperties)y.FieldType.Properties).Pattern == payload.StringProperties.Pattern + ), _cancellationToken), Times.Once()); + } +}