From 1c75f08a336539c3be494f8a615db17fc9edc8eb Mon Sep 17 00:00:00 2001 From: Steve Smith Date: Fri, 10 Jul 2020 11:50:46 -0400 Subject: [PATCH] Add catalogitem update endpoint to public api (#418) * Initial update endpoint working * Updated CatalogItem to support more updates; added tests * Got MediatR domain events working to check for duplicate item names * Adding reference link * Remove domain events spike code * clean up usings --- src/ApplicationCore/ApplicationCore.csproj | 1 + src/ApplicationCore/Entities/CatalogItem.cs | 28 ++++++++- .../DuplicateCatalogItemNameException.cs | 14 +++++ .../Create.CreateCatalogItemRequest.cs | 1 + src/PublicApi/CatalogItemEndpoints/Create.cs | 1 + .../Update.UpdateCatalogItemRequest.cs | 21 +++++++ .../Update.UpdateCatalogItemResponse.cs | 17 ++++++ src/PublicApi/CatalogItemEndpoints/Update.cs | 58 +++++++++++++++++++ src/PublicApi/PublicApi.csproj | 2 + src/PublicApi/Startup.cs | 7 ++- .../Services/CatalogItemViewModelService.cs | 2 +- src/Web/Web.csproj | 4 +- .../CatalogItemTests/UpdateDetails.cs | 56 ++++++++++++++++++ 13 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 src/ApplicationCore/Exceptions/DuplicateCatalogItemNameException.cs create mode 100644 src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemRequest.cs create mode 100644 src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemResponse.cs create mode 100644 src/PublicApi/CatalogItemEndpoints/Update.cs create mode 100644 tests/UnitTests/ApplicationCore/Entities/CatalogItemTests/UpdateDetails.cs diff --git a/src/ApplicationCore/ApplicationCore.csproj b/src/ApplicationCore/ApplicationCore.csproj index 4eb43c5..8e8906c 100644 --- a/src/ApplicationCore/ApplicationCore.csproj +++ b/src/ApplicationCore/ApplicationCore.csproj @@ -8,6 +8,7 @@ + diff --git a/src/ApplicationCore/Entities/CatalogItem.cs b/src/ApplicationCore/Entities/CatalogItem.cs index caa8562..7fe0149 100644 --- a/src/ApplicationCore/Entities/CatalogItem.cs +++ b/src/ApplicationCore/Entities/CatalogItem.cs @@ -1,8 +1,11 @@ using Ardalis.GuardClauses; using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using System.Collections.Generic; namespace Microsoft.eShopWeb.ApplicationCore.Entities { + + public class CatalogItem : BaseEntity, IAggregateRoot { public string Name { get; private set; } @@ -14,7 +17,12 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities public int CatalogBrandId { get; private set; } public CatalogBrand CatalogBrand { get; private set; } - public CatalogItem(int catalogTypeId, int catalogBrandId, string description, string name, decimal price, string pictureUri) + public CatalogItem(int catalogTypeId, + int catalogBrandId, + string description, + string name, + decimal price, + string pictureUri) { CatalogTypeId = catalogTypeId; CatalogBrandId = catalogBrandId; @@ -24,11 +32,27 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities PictureUri = pictureUri; } - public void Update(string name, decimal price) + public void UpdateDetails(string name, string description, decimal price) { Guard.Against.NullOrEmpty(name, nameof(name)); + Guard.Against.NullOrEmpty(description, nameof(description)); + Guard.Against.NegativeOrZero(price, nameof(price)); + Name = name; + Description = description; Price = price; } + + public void UpdateBrand(int catalogBrandId) + { + Guard.Against.Zero(catalogBrandId, nameof(catalogBrandId)); + CatalogBrandId = catalogBrandId; + } + + public void UpdateType(int catalogTypeId) + { + Guard.Against.Zero(catalogTypeId, nameof(catalogTypeId)); + CatalogTypeId = catalogTypeId; + } } } \ No newline at end of file diff --git a/src/ApplicationCore/Exceptions/DuplicateCatalogItemNameException.cs b/src/ApplicationCore/Exceptions/DuplicateCatalogItemNameException.cs new file mode 100644 index 0000000..09fe302 --- /dev/null +++ b/src/ApplicationCore/Exceptions/DuplicateCatalogItemNameException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Microsoft.eShopWeb.ApplicationCore.Exceptions +{ + public class DuplicateCatalogItemNameException : Exception + { + public DuplicateCatalogItemNameException(string message, int duplicateItemId) : base(message) + { + DuplicateItemId = duplicateItemId; + } + + public int DuplicateItemId { get; } + } +} diff --git a/src/PublicApi/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs index 3a8e9a4..e3193d8 100644 --- a/src/PublicApi/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs +++ b/src/PublicApi/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs @@ -9,4 +9,5 @@ public string PictureUri { get; set; } public decimal Price { get; set; } } + } diff --git a/src/PublicApi/CatalogItemEndpoints/Create.cs b/src/PublicApi/CatalogItemEndpoints/Create.cs index f7a44af..27ce2ba 100644 --- a/src/PublicApi/CatalogItemEndpoints/Create.cs +++ b/src/PublicApi/CatalogItemEndpoints/Create.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints { + [Authorize(Roles = AuthorizationConstants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public class Create : BaseAsyncEndpoint { diff --git a/src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemRequest.cs new file mode 100644 index 0000000..30b36b5 --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemRequest.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints +{ + public class UpdateCatalogItemRequest : BaseRequest + { + [Range(1, 10000)] + public int Id { get; set; } + [Range(1, 10000)] + public int CatalogBrandId { get; set; } + [Range(1, 10000)] + public int CatalogTypeId { get; set; } + [Required] + public string Description { get; set; } + [Required] + public string Name { get; set; } + public string PictureUri { get; set; } + [Range(0.01, 10000)] + public decimal Price { get; set; } + } +} diff --git a/src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemResponse.cs b/src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemResponse.cs new file mode 100644 index 0000000..12913b5 --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemResponse.cs @@ -0,0 +1,17 @@ +using System; + +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints +{ + public class UpdateCatalogItemResponse : BaseResponse + { + public UpdateCatalogItemResponse(Guid correlationId) : base(correlationId) + { + } + + public UpdateCatalogItemResponse() + { + } + + public CatalogItemDto CatalogItem { get; set; } + } +} diff --git a/src/PublicApi/CatalogItemEndpoints/Update.cs b/src/PublicApi/CatalogItemEndpoints/Update.cs new file mode 100644 index 0000000..0672006 --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/Update.cs @@ -0,0 +1,58 @@ +using Ardalis.ApiEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopWeb.ApplicationCore.Constants; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Exceptions; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using Swashbuckle.AspNetCore.Annotations; +using System; +using System.Threading.Tasks; + +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints +{ + [Authorize(Roles = AuthorizationConstants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + public class Update : BaseAsyncEndpoint + { + private readonly IAsyncRepository _itemRepository; + + public Update(IAsyncRepository itemRepository) + { + _itemRepository = itemRepository; + } + + [HttpPut("api/catalog-items")] + [SwaggerOperation( + Summary = "Updates a Catalog Item", + Description = "Updates a Catalog Item", + OperationId = "catalog-items.update", + Tags = new[] { "CatalogItemEndpoints" }) + ] + public override async Task> HandleAsync(UpdateCatalogItemRequest request) + { + var response = new UpdateCatalogItemResponse(request.CorrelationId()); + + var existingItem = await _itemRepository.GetByIdAsync(request.Id); + + existingItem.UpdateDetails(request.Name, request.Description, request.Price); + existingItem.UpdateBrand(request.CatalogBrandId); + existingItem.UpdateType(request.CatalogTypeId); + + await _itemRepository.UpdateAsync(existingItem); + + var dto = new CatalogItemDto + { + Id = existingItem.Id, + CatalogBrandId = existingItem.CatalogBrandId, + CatalogTypeId = existingItem.CatalogTypeId, + Description = existingItem.Description, + Name = existingItem.Name, + PictureUri = existingItem.PictureUri, + Price = existingItem.Price + }; + response.CatalogItem = dto; + return response; + } + } +} diff --git a/src/PublicApi/PublicApi.csproj b/src/PublicApi/PublicApi.csproj index fb7c28d..7b305d1 100644 --- a/src/PublicApi/PublicApi.csproj +++ b/src/PublicApi/PublicApi.csproj @@ -11,6 +11,8 @@ + + diff --git a/src/PublicApi/Startup.cs b/src/PublicApi/Startup.cs index 854565e..fc1d6db 100644 --- a/src/PublicApi/Startup.cs +++ b/src/PublicApi/Startup.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.Text; using AutoMapper; +using MediatR; + using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; @@ -8,6 +11,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb.ApplicationCore.Constants; +using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Services; using Microsoft.eShopWeb.Infrastructure.Data; @@ -119,9 +123,9 @@ namespace Microsoft.eShopWeb.PublicApi services.AddControllers(); + services.AddMediatR(typeof(CatalogItem).Assembly); services.AddAutoMapper(typeof(Startup).Assembly); - services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); @@ -157,7 +161,6 @@ namespace Microsoft.eShopWeb.PublicApi } }); }); - } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Web/Services/CatalogItemViewModelService.cs b/src/Web/Services/CatalogItemViewModelService.cs index 8d93585..fad2852 100644 --- a/src/Web/Services/CatalogItemViewModelService.cs +++ b/src/Web/Services/CatalogItemViewModelService.cs @@ -18,7 +18,7 @@ namespace Microsoft.eShopWeb.Web.Services public async Task UpdateCatalogItem(CatalogItemViewModel viewModel) { var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id); - existingCatalogItem.Update(viewModel.Name, viewModel.Price); + existingCatalogItem.UpdateDetails(viewModel.Name, existingCatalogItem.Description, viewModel.Price); await _catalogItemRepository.UpdateAsync(existingCatalogItem); } } diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 8dc02ce..c4185cd 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -25,8 +25,8 @@ - - + + diff --git a/tests/UnitTests/ApplicationCore/Entities/CatalogItemTests/UpdateDetails.cs b/tests/UnitTests/ApplicationCore/Entities/CatalogItemTests/UpdateDetails.cs new file mode 100644 index 0000000..d1d4600 --- /dev/null +++ b/tests/UnitTests/ApplicationCore/Entities/CatalogItemTests/UpdateDetails.cs @@ -0,0 +1,56 @@ +using Xunit; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System; + +namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.CatalogItemTests +{ + public class UpdateDetails + { + private CatalogItem _testItem; + private int _validTypeId = 1; + private int _validBrandId = 2; + private string _validDescription = "test description"; + private string _validName = "test name"; + private decimal _validPrice = 1.23m; + private string _validUri = "/123"; + + public UpdateDetails() + { + _testItem = new CatalogItem(_validTypeId, _validBrandId, _validDescription, _validName, _validPrice, _validUri); + } + + [Fact] + public void ThrowsArgumentExceptionGivenEmptyName() + { + string newValue = ""; + Assert.Throws(() => _testItem.UpdateDetails(newValue, _validDescription, _validPrice)); + } + + [Fact] + public void ThrowsArgumentExceptionGivenEmptyDescription() + { + string newValue = ""; + Assert.Throws(() => _testItem.UpdateDetails(_validName, newValue, _validPrice)); + } + + [Fact] + public void ThrowsArgumentNullExceptionGivenNullName() + { + Assert.Throws(() => _testItem.UpdateDetails(null, _validDescription, _validPrice)); + } + + [Fact] + public void ThrowsArgumentNullExceptionGivenNullDescription() + { + Assert.Throws(() => _testItem.UpdateDetails(_validName, null, _validPrice)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1.23)] + public void ThrowsArgumentExceptionGivenNonPositivePrice(decimal newPrice) + { + Assert.Throws(() => _testItem.UpdateDetails(_validName, _validDescription, newPrice)); + } + } +}