From 1e13733d3d38e1213821016eff47637a9c66f793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Michel?= Date: Fri, 21 Jan 2022 16:13:31 +0100 Subject: [PATCH] Feature/migrate to minimal api (#662) * migrate from classic controller to minimal api * fix all PublicApi integration test * update all nuget package add forget project * fix pay now * Adapt readme use in memory database * undo AuthenticateEndpoint to use EndpointBaseAsync * Update README.md Co-authored-by: Steve Smith Co-authored-by: Steve Smith --- README.md | 15 ++-- eShopOnWeb.sln | 7 ++ src/ApplicationCore/ApplicationCore.csproj | 2 +- src/BlazorAdmin/BlazorAdmin.csproj | 10 +-- src/BlazorShared/BlazorShared.csproj | 2 +- src/Infrastructure/Dependencies.cs | 40 ++++++++++ src/Infrastructure/Infrastructure.csproj | 5 +- ...thenticateEndpoint.AuthenticateRequest.cs} | 0 ...henticateEndpoint.AuthenticateResponse.cs} | 0 ....cs => AuthenticateEndpoint.ClaimValue.cs} | 0 ...fo.cs => AuthenticateEndpoint.UserInfo.cs} | 0 ...uthenticate.cs => AuthenticateEndpoint.cs} | 9 ++- ...ListEndpoint.ListCatalogBrandsResponse.cs} | 0 .../CatalogBrandListEndpoint.cs | 48 ++++++++++++ src/PublicApi/CatalogBrandEndpoints/List.cs | 44 ----------- ...tByIdEndpoint.GetByIdCatalogItemRequest.cs | 11 +++ ...yIdEndpoint.GetByIdCatalogItemResponse.cs} | 0 .../CatalogItemGetByIdEndpoint.cs | 56 ++++++++++++++ ...gedEndpoint.ListPagedCatalogItemRequest.cs | 17 +++++ ...dEndpoint.ListPagedCatalogItemResponse.cs} | 0 ...ged.cs => CatalogItemListPagedEndpoint.cs} | 55 +++++++------- ...gItemEndpoint.CreateCatalogItemRequest.cs} | 0 ...ItemEndpoint.CreateCatalogItemResponse.cs} | 0 ...Create.cs => CreateCatalogItemEndpoint.cs} | 56 +++++++------- .../Delete.DeleteCatalogItemRequest.cs | 9 --- src/PublicApi/CatalogItemEndpoints/Delete.cs | 43 ----------- ...ogItemEndpoint.DeleteCatalogItemRequest.cs | 11 +++ ...ItemEndpoint.DeleteCatalogItemResponse.cs} | 0 .../DeleteCatalogItemEndpoint.cs | 45 +++++++++++ .../GetById.GetByIdCatalogItemRequest.cs | 6 -- src/PublicApi/CatalogItemEndpoints/GetById.cs | 50 ------------- .../ListPaged.ListPagedCatalogItemRequest.cs | 9 --- src/PublicApi/CatalogItemEndpoints/Update.cs | 60 --------------- ...gItemEndpoint.UpdateCatalogItemRequest.cs} | 0 ...ItemEndpoint.UpdateCatalogItemResponse.cs} | 0 .../UpdateCatalogItemEndpoint.cs | 64 ++++++++++++++++ ...eListEndpoint.ListCatalogTypesResponse.cs} | 0 .../CatalogTypeListEndpoint.cs | 48 ++++++++++++ src/PublicApi/CatalogTypeEndpoints/List.cs | 44 ----------- src/PublicApi/Program.cs | 22 +++--- src/PublicApi/PublicApi.csproj | 17 +++-- src/Web/Pages/Basket/BasketItemViewModel.cs | 4 +- src/Web/Program.cs | 11 +-- src/Web/Services/BasketViewModelService.cs | 5 +- src/Web/Web.csproj | 18 ++--- src/Web/appsettings.json | 2 +- tests/FunctionalTests/FunctionalTests.csproj | 6 +- .../PublicApi/ApiTestFixture.cs | 2 +- .../AuthEndpoints/AuthenticateEndpoint.cs | 74 +++++++++---------- .../ApiCatalogControllerList.cs | 41 ---------- .../CatalogItemEndpoints/CreateEndpoint.cs | 74 ------------------- .../CatalogItemEndpoints/DeleteEndpoint.cs | 47 ------------ .../CatalogItemEndpoints/GetByIdEndpoint.cs | 42 ----------- .../IntegrationTests/IntegrationTests.csproj | 2 +- .../ApiTokenHelper.cs | 50 +++++++++++++ .../AuthEndpoints/AuthenticateEndpointTest.cs | 36 +++++++++ .../CatalogItemGetByIdEndpointTest.cs | 32 ++++++++ .../CatalogItemListPagedEndpoint.cs | 49 ++++++++++++ .../CreateCatalogItemEndpointTest.cs | 69 +++++++++++++++++ .../DeleteCatalogItemEndpointTest.cs | 38 ++++++++++ .../PublicApiIntegrationTests/ProgramTest.cs | 27 +++++++ .../PublicApiIntegrationTests.csproj | 35 +++++++++ .../appsettings.json | 3 + 63 files changed, 842 insertions(+), 630 deletions(-) create mode 100644 src/Infrastructure/Dependencies.cs rename src/PublicApi/AuthEndpoints/{Authenticate.AuthenticateRequest.cs => AuthenticateEndpoint.AuthenticateRequest.cs} (100%) rename src/PublicApi/AuthEndpoints/{Authenticate.AuthenticateResponse.cs => AuthenticateEndpoint.AuthenticateResponse.cs} (100%) rename src/PublicApi/AuthEndpoints/{Authenticate.ClaimValue.cs => AuthenticateEndpoint.ClaimValue.cs} (100%) rename src/PublicApi/AuthEndpoints/{Authenticate.UserInfo.cs => AuthenticateEndpoint.UserInfo.cs} (100%) rename src/PublicApi/AuthEndpoints/{Authenticate.cs => AuthenticateEndpoint.cs} (90%) rename src/PublicApi/CatalogBrandEndpoints/{List.ListCatalogBrandsResponse.cs => CatalogBrandListEndpoint.ListCatalogBrandsResponse.cs} (100%) create mode 100644 src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs delete mode 100644 src/PublicApi/CatalogBrandEndpoints/List.cs create mode 100644 src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemRequest.cs rename src/PublicApi/CatalogItemEndpoints/{GetById.GetByIdCatalogItemResponse.cs => CatalogItemGetByIdEndpoint.GetByIdCatalogItemResponse.cs} (100%) create mode 100644 src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs create mode 100644 src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs rename src/PublicApi/CatalogItemEndpoints/{ListPaged.ListPagedCatalogItemResponse.cs => CatalogItemListPagedEndpoint.ListPagedCatalogItemResponse.cs} (100%) rename src/PublicApi/CatalogItemEndpoints/{ListPaged.cs => CatalogItemListPagedEndpoint.cs} (50%) rename src/PublicApi/CatalogItemEndpoints/{Create.CreateCatalogItemRequest.cs => CreateCatalogItemEndpoint.CreateCatalogItemRequest.cs} (100%) rename src/PublicApi/CatalogItemEndpoints/{Create.CreateCatalogItemResponse.cs => CreateCatalogItemEndpoint.CreateCatalogItemResponse.cs} (100%) rename src/PublicApi/CatalogItemEndpoints/{Create.cs => CreateCatalogItemEndpoint.cs} (59%) delete mode 100644 src/PublicApi/CatalogItemEndpoints/Delete.DeleteCatalogItemRequest.cs delete mode 100644 src/PublicApi/CatalogItemEndpoints/Delete.cs create mode 100644 src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemRequest.cs rename src/PublicApi/CatalogItemEndpoints/{Delete.DeleteCatalogItemResponse.cs => DeleteCatalogItemEndpoint.DeleteCatalogItemResponse.cs} (100%) create mode 100644 src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs delete mode 100644 src/PublicApi/CatalogItemEndpoints/GetById.GetByIdCatalogItemRequest.cs delete mode 100644 src/PublicApi/CatalogItemEndpoints/GetById.cs delete mode 100644 src/PublicApi/CatalogItemEndpoints/ListPaged.ListPagedCatalogItemRequest.cs delete mode 100644 src/PublicApi/CatalogItemEndpoints/Update.cs rename src/PublicApi/CatalogItemEndpoints/{Update.UpdateCatalogItemRequest.cs => UpdateCatalogItemEndpoint.UpdateCatalogItemRequest.cs} (100%) rename src/PublicApi/CatalogItemEndpoints/{Update.UpdateCatalogItemResponse.cs => UpdateCatalogItemEndpoint.UpdateCatalogItemResponse.cs} (100%) create mode 100644 src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs rename src/PublicApi/CatalogTypeEndpoints/{List.ListCatalogTypesResponse.cs => CatalogTypeListEndpoint.ListCatalogTypesResponse.cs} (100%) create mode 100644 src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs delete mode 100644 src/PublicApi/CatalogTypeEndpoints/List.cs delete mode 100644 tests/FunctionalTests/PublicApi/CatalogItemEndpoints/ApiCatalogControllerList.cs delete mode 100644 tests/FunctionalTests/PublicApi/CatalogItemEndpoints/CreateEndpoint.cs delete mode 100644 tests/FunctionalTests/PublicApi/CatalogItemEndpoints/DeleteEndpoint.cs delete mode 100644 tests/FunctionalTests/PublicApi/CatalogItemEndpoints/GetByIdEndpoint.cs create mode 100644 tests/PublicApiIntegrationTests/ApiTokenHelper.cs create mode 100644 tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs create mode 100644 tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs create mode 100644 tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs create mode 100644 tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs create mode 100644 tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs create mode 100644 tests/PublicApiIntegrationTests/ProgramTest.cs create mode 100644 tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj create mode 100644 tests/PublicApiIntegrationTests/appsettings.json diff --git a/README.md b/README.md index d7466a9b8..60591b040 100644 --- a/README.md +++ b/README.md @@ -55,18 +55,13 @@ You can also run the samples in Docker (see below). ### Configuring the sample to use SQL Server -1. Update `Startup.cs`'s `ConfigureDevelopmentServices` method as follows: +1. By default, the project uses a real database. If you want an in memory database, you can add in `appsettings.json` - ```csharp - public void ConfigureDevelopmentServices(IServiceCollection services) - { - // use in-memory database - //ConfigureTestingServices(services); + ```json + { + "UseOnlyInMemoryDatabase": true + } - // use real database - ConfigureProductionServices(services); - - } ``` 1. Ensure your connection strings in `appsettings.json` point to a local SQL Server instance. diff --git a/eShopOnWeb.sln b/eShopOnWeb.sln index 0394e30ba..2adaaf8ca 100755 --- a/eShopOnWeb.sln +++ b/eShopOnWeb.sln @@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorAdmin", "src\BlazorAd EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorShared", "src\BlazorShared\BlazorShared.csproj", "{715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApiIntegrationTests", "tests\PublicApiIntegrationTests\PublicApiIntegrationTests.csproj", "{D53EF010-8F8C-4337-A059-456E19D8AE63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +86,10 @@ Global {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9}.Release|Any CPU.Build.0 = Release|Any CPU + {D53EF010-8F8C-4337-A059-456E19D8AE63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D53EF010-8F8C-4337-A059-456E19D8AE63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D53EF010-8F8C-4337-A059-456E19D8AE63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D53EF010-8F8C-4337-A059-456E19D8AE63}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -98,6 +104,7 @@ Global {B5E4F33C-4667-4A55-AF6A-740F84C4CF3A} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {71368733-80A4-4869-B215-3A7001878577} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} {715CF7AF-A1EE-40A6-94A0-8DA3F3B2CAE9} = {419A6ACE-0419-4315-A6FB-B0E63D39432E} + {D53EF010-8F8C-4337-A059-456E19D8AE63} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B} diff --git a/src/ApplicationCore/ApplicationCore.csproj b/src/ApplicationCore/ApplicationCore.csproj index 6b310b54c..3e939d281 100644 --- a/src/ApplicationCore/ApplicationCore.csproj +++ b/src/ApplicationCore/ApplicationCore.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/BlazorAdmin/BlazorAdmin.csproj b/src/BlazorAdmin/BlazorAdmin.csproj index f74f10291..19347c39a 100644 --- a/src/BlazorAdmin/BlazorAdmin.csproj +++ b/src/BlazorAdmin/BlazorAdmin.csproj @@ -7,11 +7,11 @@ - - - - - + + + + + diff --git a/src/BlazorShared/BlazorShared.csproj b/src/BlazorShared/BlazorShared.csproj index a8f1800e6..9717973a3 100644 --- a/src/BlazorShared/BlazorShared.csproj +++ b/src/BlazorShared/BlazorShared.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Infrastructure/Dependencies.cs b/src/Infrastructure/Dependencies.cs new file mode 100644 index 000000000..d049a962b --- /dev/null +++ b/src/Infrastructure/Dependencies.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.Infrastructure.Data; +using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.eShopWeb.Infrastructure; + +public static class Dependencies +{ + public static void ConfigureServices(IConfiguration configuration, IServiceCollection services) + { + var useOnlyInMemoryDatabase = false; + if (configuration["UseOnlyInMemoryDatabase"] != null) + { + useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]); + } + + if (useOnlyInMemoryDatabase) + { + services.AddDbContext(c => + c.UseInMemoryDatabase("Catalog")); + + services.AddDbContext(options => + options.UseInMemoryDatabase("Identity")); + } + else + { + // use real database + // Requires LocalDB which can be installed with SQL Server Express 2016 + // https://www.microsoft.com/en-us/download/details.aspx?id=54284 + services.AddDbContext(c => + c.UseSqlServer(configuration.GetConnectionString("CatalogConnection"))); + + // Add Identity DbContext + services.AddDbContext(options => + options.UseSqlServer(configuration.GetConnectionString("IdentityConnection"))); + } + } +} diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 41ac093f9..922227619 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -7,8 +7,9 @@ - - + + + diff --git a/src/PublicApi/AuthEndpoints/Authenticate.AuthenticateRequest.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateRequest.cs similarity index 100% rename from src/PublicApi/AuthEndpoints/Authenticate.AuthenticateRequest.cs rename to src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateRequest.cs diff --git a/src/PublicApi/AuthEndpoints/Authenticate.AuthenticateResponse.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateResponse.cs similarity index 100% rename from src/PublicApi/AuthEndpoints/Authenticate.AuthenticateResponse.cs rename to src/PublicApi/AuthEndpoints/AuthenticateEndpoint.AuthenticateResponse.cs diff --git a/src/PublicApi/AuthEndpoints/Authenticate.ClaimValue.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs similarity index 100% rename from src/PublicApi/AuthEndpoints/Authenticate.ClaimValue.cs rename to src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs diff --git a/src/PublicApi/AuthEndpoints/Authenticate.UserInfo.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs similarity index 100% rename from src/PublicApi/AuthEndpoints/Authenticate.UserInfo.cs rename to src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs diff --git a/src/PublicApi/AuthEndpoints/Authenticate.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs similarity index 90% rename from src/PublicApi/AuthEndpoints/Authenticate.cs rename to src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs index eee5218d2..0d5bece31 100644 --- a/src/PublicApi/AuthEndpoints/Authenticate.cs +++ b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs @@ -9,14 +9,17 @@ namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; -public class Authenticate : EndpointBaseAsync +/// +/// Authenticates a user +/// +public class AuthenticateEndpoint : EndpointBaseAsync .WithRequest .WithActionResult { private readonly SignInManager _signInManager; private readonly ITokenClaimsService _tokenClaimsService; - public Authenticate(SignInManager signInManager, + public AuthenticateEndpoint(SignInManager signInManager, ITokenClaimsService tokenClaimsService) { _signInManager = signInManager; @@ -30,7 +33,7 @@ public Authenticate(SignInManager signInManager, OperationId = "auth.authenticate", Tags = new[] { "AuthEndpoints" }) ] - public override async Task> HandleAsync(AuthenticateRequest request, CancellationToken cancellationToken) + public override async Task> HandleAsync(AuthenticateRequest request, CancellationToken cancellationToken = default) { var response = new AuthenticateResponse(request.CorrelationId()); diff --git a/src/PublicApi/CatalogBrandEndpoints/List.ListCatalogBrandsResponse.cs b/src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.ListCatalogBrandsResponse.cs similarity index 100% rename from src/PublicApi/CatalogBrandEndpoints/List.ListCatalogBrandsResponse.cs rename to src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.ListCatalogBrandsResponse.cs diff --git a/src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs b/src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs new file mode 100644 index 000000000..dce823e7c --- /dev/null +++ b/src/PublicApi/CatalogBrandEndpoints/CatalogBrandListEndpoint.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using MinimalApi.Endpoint; + +namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; + +/// +/// List Catalog Brands +/// +public class CatalogBrandListEndpoint : IEndpoint +{ + private IRepository _catalogBrandRepository; + private readonly IMapper _mapper; + + public CatalogBrandListEndpoint(IMapper mapper) + { + _mapper = mapper; + } + + public void AddRoute(IEndpointRouteBuilder app) + { + app.MapGet("api/catalog-brands", + async (IRepository catalogBrandRepository) => + { + _catalogBrandRepository = catalogBrandRepository; + return await HandleAsync(); + }) + .Produces() + .WithTags("CatalogBrandEndpoints"); + } + + public async Task HandleAsync() + { + var response = new ListCatalogBrandsResponse(); + + var items = await _catalogBrandRepository.ListAsync(); + + response.CatalogBrands.AddRange(items.Select(_mapper.Map)); + + return Results.Ok(response); + } +} diff --git a/src/PublicApi/CatalogBrandEndpoints/List.cs b/src/PublicApi/CatalogBrandEndpoints/List.cs deleted file mode 100644 index d64b8ca13..000000000 --- a/src/PublicApi/CatalogBrandEndpoints/List.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Ardalis.ApiEndpoints; -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Swashbuckle.AspNetCore.Annotations; - -namespace Microsoft.eShopWeb.PublicApi.CatalogBrandEndpoints; - -public class List : EndpointBaseAsync - .WithoutRequest - .WithActionResult -{ - private readonly IRepository _catalogBrandRepository; - private readonly IMapper _mapper; - - public List(IRepository catalogBrandRepository, - IMapper mapper) - { - _catalogBrandRepository = catalogBrandRepository; - _mapper = mapper; - } - - [HttpGet("api/catalog-brands")] - [SwaggerOperation( - Summary = "List Catalog Brands", - Description = "List Catalog Brands", - OperationId = "catalog-brands.List", - Tags = new[] { "CatalogBrandEndpoints" }) - ] - public override async Task> HandleAsync(CancellationToken cancellationToken) - { - var response = new ListCatalogBrandsResponse(); - - var items = await _catalogBrandRepository.ListAsync(cancellationToken); - - response.CatalogBrands.AddRange(items.Select(_mapper.Map)); - - return Ok(response); - } -} diff --git a/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemRequest.cs new file mode 100644 index 000000000..a53f5ef6a --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemRequest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; + +public class GetByIdCatalogItemRequest : BaseRequest +{ + public int CatalogItemId { get; init; } + + public GetByIdCatalogItemRequest(int catalogItemId) + { + CatalogItemId = catalogItemId; + } +} diff --git a/src/PublicApi/CatalogItemEndpoints/GetById.GetByIdCatalogItemResponse.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemResponse.cs similarity index 100% rename from src/PublicApi/CatalogItemEndpoints/GetById.GetByIdCatalogItemResponse.cs rename to src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.GetByIdCatalogItemResponse.cs diff --git a/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs new file mode 100644 index 000000000..a1f601116 --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemGetByIdEndpoint.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using MinimalApi.Endpoint; + +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; + +/// +/// Get a Catalog Item by Id +/// +public class CatalogItemGetByIdEndpoint : IEndpoint +{ + private IRepository _itemRepository; + private readonly IUriComposer _uriComposer; + + public CatalogItemGetByIdEndpoint(IUriComposer uriComposer) + { + _uriComposer = uriComposer; + } + + public void AddRoute(IEndpointRouteBuilder app) + { + app.MapGet("api/catalog-items/{catalogItemId}", + async (int catalogItemId, IRepository itemRepository) => + { + _itemRepository = itemRepository; + return await HandleAsync(new GetByIdCatalogItemRequest(catalogItemId)); + }) + .Produces() + .WithTags("CatalogItemEndpoints"); + } + + public async Task HandleAsync(GetByIdCatalogItemRequest request) + { + var response = new GetByIdCatalogItemResponse(request.CorrelationId()); + + var item = await _itemRepository.GetByIdAsync(request.CatalogItemId); + if (item is null) + return Results.NotFound(); + + response.CatalogItem = new CatalogItemDto + { + Id = item.Id, + CatalogBrandId = item.CatalogBrandId, + CatalogTypeId = item.CatalogTypeId, + Description = item.Description, + Name = item.Name, + PictureUri = _uriComposer.ComposePicUri(item.PictureUri), + Price = item.Price + }; + return Results.Ok(response); + } +} diff --git a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs new file mode 100644 index 000000000..e9744d8bb --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs @@ -0,0 +1,17 @@ +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; + +public class ListPagedCatalogItemRequest : BaseRequest +{ + public int? PageSize { get; init; } + public int? PageIndex { get; init; } + public int? CatalogBrandId { get; init; } + public int? CatalogTypeId { get; init; } + + public ListPagedCatalogItemRequest(int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId) + { + PageSize = pageSize ?? 0; + PageIndex = pageIndex ?? 0; + CatalogBrandId = catalogBrandId; + CatalogTypeId = catalogTypeId; + } +} diff --git a/src/PublicApi/CatalogItemEndpoints/ListPaged.ListPagedCatalogItemResponse.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemResponse.cs similarity index 100% rename from src/PublicApi/CatalogItemEndpoints/ListPaged.ListPagedCatalogItemResponse.cs rename to src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemResponse.cs diff --git a/src/PublicApi/CatalogItemEndpoints/ListPaged.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs similarity index 50% rename from src/PublicApi/CatalogItemEndpoints/ListPaged.cs rename to src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs index c7c20f761..308d31069 100644 --- a/src/PublicApi/CatalogItemEndpoints/ListPaged.cs +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs @@ -1,55 +1,58 @@ using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using Ardalis.ApiEndpoints; using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; -using Swashbuckle.AspNetCore.Annotations; +using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; -public class ListPaged : EndpointBaseAsync - .WithRequest - .WithActionResult +/// +/// List Catalog Items (paged) +/// +public class CatalogItemListPagedEndpoint : IEndpoint { - private readonly IRepository _itemRepository; + private IRepository _itemRepository; private readonly IUriComposer _uriComposer; private readonly IMapper _mapper; - public ListPaged(IRepository itemRepository, - IUriComposer uriComposer, - IMapper mapper) + public CatalogItemListPagedEndpoint(IUriComposer uriComposer, IMapper mapper) { - _itemRepository = itemRepository; _uriComposer = uriComposer; _mapper = mapper; } - [HttpGet("api/catalog-items")] - [SwaggerOperation( - Summary = "List Catalog Items (paged)", - Description = "List Catalog Items (paged)", - OperationId = "catalog-items.ListPaged", - Tags = new[] { "CatalogItemEndpoints" }) - ] - public override async Task> HandleAsync([FromQuery] ListPagedCatalogItemRequest request, CancellationToken cancellationToken) + public void AddRoute(IEndpointRouteBuilder app) + { + app.MapGet("api/catalog-items", + async (int? pageSize, int? pageIndex, int? catalogBrandId, int? catalogTypeId, IRepository itemRepository) => + { + _itemRepository = itemRepository; + return await HandleAsync(new ListPagedCatalogItemRequest(pageSize, pageIndex, catalogBrandId, catalogTypeId)); + }) + .Produces() + .WithTags("CatalogItemEndpoints"); + } + + public async Task HandleAsync(ListPagedCatalogItemRequest request) { var response = new ListPagedCatalogItemResponse(request.CorrelationId()); var filterSpec = new CatalogFilterSpecification(request.CatalogBrandId, request.CatalogTypeId); - int totalItems = await _itemRepository.CountAsync(filterSpec, cancellationToken); + int totalItems = await _itemRepository.CountAsync(filterSpec); var pagedSpec = new CatalogFilterPaginatedSpecification( - skip: request.PageIndex * request.PageSize, - take: request.PageSize, + skip: request.PageIndex.Value * request.PageSize.Value, + take: request.PageSize.Value, brandId: request.CatalogBrandId, typeId: request.CatalogTypeId); - var items = await _itemRepository.ListAsync(pagedSpec, cancellationToken); + var items = await _itemRepository.ListAsync(pagedSpec); response.CatalogItems.AddRange(items.Select(_mapper.Map)); foreach (CatalogItemDto item in response.CatalogItems) @@ -59,13 +62,13 @@ public override async Task> HandleAsy if (request.PageSize > 0) { - response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize).ToString()); + response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize.Value).ToString()); } else { response.PageCount = totalItems > 0 ? 1 : 0; } - return Ok(response); + return Results.Ok(response); } } diff --git a/src/PublicApi/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.CreateCatalogItemRequest.cs similarity index 100% rename from src/PublicApi/CatalogItemEndpoints/Create.CreateCatalogItemRequest.cs rename to src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.CreateCatalogItemRequest.cs diff --git a/src/PublicApi/CatalogItemEndpoints/Create.CreateCatalogItemResponse.cs b/src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.CreateCatalogItemResponse.cs similarity index 100% rename from src/PublicApi/CatalogItemEndpoints/Create.CreateCatalogItemResponse.cs rename to src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.CreateCatalogItemResponse.cs diff --git a/src/PublicApi/CatalogItemEndpoints/Create.cs b/src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs similarity index 59% rename from src/PublicApi/CatalogItemEndpoints/Create.cs rename to src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs index da9b885ab..25527f93b 100644 --- a/src/PublicApi/CatalogItemEndpoints/Create.cs +++ b/src/PublicApi/CatalogItemEndpoints/CreateCatalogItemEndpoint.cs @@ -1,52 +1,56 @@ -using System.Threading; -using System.Threading.Tasks; -using Ardalis.ApiEndpoints; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Exceptions; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; -using Swashbuckle.AspNetCore.Annotations; +using MinimalApi.Endpoint; namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; -[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class Create : EndpointBaseAsync - .WithRequest - .WithActionResult +/// +/// Creates a new Catalog Item +/// +public class CreateCatalogItemEndpoint : IEndpoint { - private readonly IRepository _itemRepository; + private IRepository _itemRepository; private readonly IUriComposer _uriComposer; - public Create(IRepository itemRepository, - IUriComposer uriComposer) + public CreateCatalogItemEndpoint(IUriComposer uriComposer) { - _itemRepository = itemRepository; _uriComposer = uriComposer; } - [HttpPost("api/catalog-items")] - [SwaggerOperation( - Summary = "Creates a new Catalog Item", - Description = "Creates a new Catalog Item", - OperationId = "catalog-items.create", - Tags = new[] { "CatalogItemEndpoints" }) - ] - public override async Task> HandleAsync(CreateCatalogItemRequest request, CancellationToken cancellationToken) + public void AddRoute(IEndpointRouteBuilder app) + { + app.MapPost("api/catalog-items", + [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async + (CreateCatalogItemRequest request, IRepository itemRepository) => + { + _itemRepository = itemRepository; + return await HandleAsync(request); + }) + .Produces() + .WithTags("CatalogItemEndpoints"); + } + + public async Task HandleAsync(CreateCatalogItemRequest request) { var response = new CreateCatalogItemResponse(request.CorrelationId()); var catalogItemNameSpecification = new CatalogItemNameSpecification(request.Name); - var existingCataloogItem = await _itemRepository.CountAsync(catalogItemNameSpecification, cancellationToken); + var existingCataloogItem = await _itemRepository.CountAsync(catalogItemNameSpecification); if (existingCataloogItem > 0) { throw new DuplicateException($"A catalogItem with name {request.Name} already exists"); } var newItem = new CatalogItem(request.CatalogTypeId, request.CatalogBrandId, request.Description, request.Name, request.Price, request.PictureUri); - newItem = await _itemRepository.AddAsync(newItem, cancellationToken); + newItem = await _itemRepository.AddAsync(newItem); if (newItem.Id != 0) { @@ -55,7 +59,7 @@ public override async Task> HandleAsync( // In production, we recommend uploading to a blob storage and deliver the image via CDN after a verification process. newItem.UpdatePictureUri("eCatalog-item-default.png"); - await _itemRepository.UpdateAsync(newItem, cancellationToken); + await _itemRepository.UpdateAsync(newItem); } var dto = new CatalogItemDto @@ -69,8 +73,6 @@ public override async Task> HandleAsync( Price = newItem.Price }; response.CatalogItem = dto; - return response; + return Results.Created($"api/catalog-items/{dto.Id}", response); } - - } diff --git a/src/PublicApi/CatalogItemEndpoints/Delete.DeleteCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/Delete.DeleteCatalogItemRequest.cs deleted file mode 100644 index 4fee76f83..000000000 --- a/src/PublicApi/CatalogItemEndpoints/Delete.DeleteCatalogItemRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; - -public class DeleteCatalogItemRequest : BaseRequest -{ - //[FromRoute] - public int CatalogItemId { get; set; } -} diff --git a/src/PublicApi/CatalogItemEndpoints/Delete.cs b/src/PublicApi/CatalogItemEndpoints/Delete.cs deleted file mode 100644 index 1724bc41c..000000000 --- a/src/PublicApi/CatalogItemEndpoints/Delete.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Ardalis.ApiEndpoints; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Swashbuckle.AspNetCore.Annotations; - -namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; - -[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class Delete : EndpointBaseAsync - .WithRequest - .WithActionResult -{ - private readonly IRepository _itemRepository; - - public Delete(IRepository itemRepository) - { - _itemRepository = itemRepository; - } - - [HttpDelete("api/catalog-items/{CatalogItemId}")] - [SwaggerOperation( - Summary = "Deletes a Catalog Item", - Description = "Deletes a Catalog Item", - OperationId = "catalog-items.Delete", - Tags = new[] { "CatalogItemEndpoints" }) - ] - public override async Task> HandleAsync([FromRoute] DeleteCatalogItemRequest request, CancellationToken cancellationToken) - { - var response = new DeleteCatalogItemResponse(request.CorrelationId()); - - var itemToDelete = await _itemRepository.GetByIdAsync(request.CatalogItemId, cancellationToken); - if (itemToDelete is null) return NotFound(); - - await _itemRepository.DeleteAsync(itemToDelete, cancellationToken); - - return Ok(response); - } -} diff --git a/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemRequest.cs new file mode 100644 index 000000000..cfdbb15dc --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemRequest.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; + +public class DeleteCatalogItemRequest : BaseRequest +{ + public int CatalogItemId { get; init; } + + public DeleteCatalogItemRequest(int catalogItemId) + { + CatalogItemId = catalogItemId; + } +} diff --git a/src/PublicApi/CatalogItemEndpoints/Delete.DeleteCatalogItemResponse.cs b/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemResponse.cs similarity index 100% rename from src/PublicApi/CatalogItemEndpoints/Delete.DeleteCatalogItemResponse.cs rename to src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.DeleteCatalogItemResponse.cs diff --git a/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs new file mode 100644 index 000000000..2a0d3e64d --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/DeleteCatalogItemEndpoint.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using MinimalApi.Endpoint; + +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; + +/// +/// Deletes a Catalog Item +/// +public class DeleteCatalogItemEndpoint : IEndpoint +{ + private IRepository _itemRepository; + + public void AddRoute(IEndpointRouteBuilder app) + { + app.MapDelete("api/catalog-items/{catalogItemId}", + [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async + (int catalogItemId, IRepository itemRepository) => + { + _itemRepository = itemRepository; + return await HandleAsync(new DeleteCatalogItemRequest(catalogItemId)); + }) + .Produces() + .WithTags("CatalogItemEndpoints"); + } + + public async Task HandleAsync(DeleteCatalogItemRequest request) + { + var response = new DeleteCatalogItemResponse(request.CorrelationId()); + + var itemToDelete = await _itemRepository.GetByIdAsync(request.CatalogItemId); + if (itemToDelete is null) + return Results.NotFound(); + + await _itemRepository.DeleteAsync(itemToDelete); + + return Results.Ok(response); + } +} diff --git a/src/PublicApi/CatalogItemEndpoints/GetById.GetByIdCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/GetById.GetByIdCatalogItemRequest.cs deleted file mode 100644 index ab0b2fb92..000000000 --- a/src/PublicApi/CatalogItemEndpoints/GetById.GetByIdCatalogItemRequest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; - -public class GetByIdCatalogItemRequest : BaseRequest -{ - public int CatalogItemId { get; set; } -} diff --git a/src/PublicApi/CatalogItemEndpoints/GetById.cs b/src/PublicApi/CatalogItemEndpoints/GetById.cs deleted file mode 100644 index 62352cfef..000000000 --- a/src/PublicApi/CatalogItemEndpoints/GetById.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Ardalis.ApiEndpoints; -using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Swashbuckle.AspNetCore.Annotations; - -namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; - -public class GetById : EndpointBaseAsync - .WithRequest - .WithActionResult -{ - private readonly IRepository _itemRepository; - private readonly IUriComposer _uriComposer; - - public GetById(IRepository itemRepository, IUriComposer uriComposer) - { - _itemRepository = itemRepository; - _uriComposer = uriComposer; - } - - [HttpGet("api/catalog-items/{CatalogItemId}")] - [SwaggerOperation( - Summary = "Get a Catalog Item by Id", - Description = "Gets a Catalog Item by Id", - OperationId = "catalog-items.GetById", - Tags = new[] { "CatalogItemEndpoints" }) - ] - public override async Task> HandleAsync([FromRoute] GetByIdCatalogItemRequest request, CancellationToken cancellationToken) - { - var response = new GetByIdCatalogItemResponse(request.CorrelationId()); - - var item = await _itemRepository.GetByIdAsync(request.CatalogItemId, cancellationToken); - if (item is null) return NotFound(); - - response.CatalogItem = new CatalogItemDto - { - Id = item.Id, - CatalogBrandId = item.CatalogBrandId, - CatalogTypeId = item.CatalogTypeId, - Description = item.Description, - Name = item.Name, - PictureUri = _uriComposer.ComposePicUri(item.PictureUri), - Price = item.Price - }; - return Ok(response); - } -} diff --git a/src/PublicApi/CatalogItemEndpoints/ListPaged.ListPagedCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/ListPaged.ListPagedCatalogItemRequest.cs deleted file mode 100644 index 7c48e9b61..000000000 --- a/src/PublicApi/CatalogItemEndpoints/ListPaged.ListPagedCatalogItemRequest.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; - -public class ListPagedCatalogItemRequest : BaseRequest -{ - public int PageSize { get; set; } - public int PageIndex { get; set; } - public int? CatalogBrandId { get; set; } - public int? CatalogTypeId { get; set; } -} diff --git a/src/PublicApi/CatalogItemEndpoints/Update.cs b/src/PublicApi/CatalogItemEndpoints/Update.cs deleted file mode 100644 index a960162ab..000000000 --- a/src/PublicApi/CatalogItemEndpoints/Update.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Ardalis.ApiEndpoints; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Swashbuckle.AspNetCore.Annotations; - -namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; - -[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] -public class Update : EndpointBaseAsync - .WithRequest - .WithActionResult -{ - private readonly IRepository _itemRepository; - private readonly IUriComposer _uriComposer; - - public Update(IRepository itemRepository, IUriComposer uriComposer) - { - _itemRepository = itemRepository; - _uriComposer = uriComposer; - } - - [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, CancellationToken cancellationToken) - { - var response = new UpdateCatalogItemResponse(request.CorrelationId()); - - var existingItem = await _itemRepository.GetByIdAsync(request.Id, cancellationToken); - - existingItem.UpdateDetails(request.Name, request.Description, request.Price); - existingItem.UpdateBrand(request.CatalogBrandId); - existingItem.UpdateType(request.CatalogTypeId); - - await _itemRepository.UpdateAsync(existingItem, cancellationToken); - - var dto = new CatalogItemDto - { - Id = existingItem.Id, - CatalogBrandId = existingItem.CatalogBrandId, - CatalogTypeId = existingItem.CatalogTypeId, - Description = existingItem.Description, - Name = existingItem.Name, - PictureUri = _uriComposer.ComposePicUri(existingItem.PictureUri), - Price = existingItem.Price - }; - response.CatalogItem = dto; - return response; - } -} diff --git a/src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.UpdateCatalogItemRequest.cs similarity index 100% rename from src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemRequest.cs rename to src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.UpdateCatalogItemRequest.cs diff --git a/src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemResponse.cs b/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.UpdateCatalogItemResponse.cs similarity index 100% rename from src/PublicApi/CatalogItemEndpoints/Update.UpdateCatalogItemResponse.cs rename to src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.UpdateCatalogItemResponse.cs diff --git a/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs new file mode 100644 index 000000000..7bc715b25 --- /dev/null +++ b/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs @@ -0,0 +1,64 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using MinimalApi.Endpoint; + +namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; + +/// +/// Updates a Catalog Item +/// +public class UpdateCatalogItemEndpoint : IEndpoint +{ + private IRepository _itemRepository; + private readonly IUriComposer _uriComposer; + + public UpdateCatalogItemEndpoint(IUriComposer uriComposer) + { + _uriComposer = uriComposer; + } + + public void AddRoute(IEndpointRouteBuilder app) + { + app.MapPut("api/catalog-items", + [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] async + (UpdateCatalogItemRequest request, IRepository itemRepository) => + { + _itemRepository = itemRepository; + return await HandleAsync(request); + }) + .Produces() + .WithTags("CatalogItemEndpoints"); + } + + public 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 = _uriComposer.ComposePicUri(existingItem.PictureUri), + Price = existingItem.Price + }; + response.CatalogItem = dto; + return Results.Ok(response); + } +} diff --git a/src/PublicApi/CatalogTypeEndpoints/List.ListCatalogTypesResponse.cs b/src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.ListCatalogTypesResponse.cs similarity index 100% rename from src/PublicApi/CatalogTypeEndpoints/List.ListCatalogTypesResponse.cs rename to src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.ListCatalogTypesResponse.cs diff --git a/src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs b/src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs new file mode 100644 index 000000000..87aa03549 --- /dev/null +++ b/src/PublicApi/CatalogTypeEndpoints/CatalogTypeListEndpoint.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Interfaces; +using MinimalApi.Endpoint; + +namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; + +/// +/// List Catalog Types +/// +public class CatalogTypeListEndpoint : IEndpoint +{ + private IRepository _catalogTypeRepository; + private readonly IMapper _mapper; + + public CatalogTypeListEndpoint(IMapper mapper) + { + _mapper = mapper; + } + + public void AddRoute(IEndpointRouteBuilder app) + { + app.MapGet("api/catalog-types", + async (IRepository catalogTypeRepository) => + { + _catalogTypeRepository = catalogTypeRepository; + return await HandleAsync(); + }) + .Produces() + .WithTags("CatalogTypeEndpoints"); + } + + public async Task HandleAsync() + { + var response = new ListCatalogTypesResponse(); + + var items = await _catalogTypeRepository.ListAsync(); + + response.CatalogTypes.AddRange(items.Select(_mapper.Map)); + + return Results.Ok(response); + } +} diff --git a/src/PublicApi/CatalogTypeEndpoints/List.cs b/src/PublicApi/CatalogTypeEndpoints/List.cs deleted file mode 100644 index eb1b21231..000000000 --- a/src/PublicApi/CatalogTypeEndpoints/List.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Ardalis.ApiEndpoints; -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopWeb.ApplicationCore.Entities; -using Microsoft.eShopWeb.ApplicationCore.Interfaces; -using Swashbuckle.AspNetCore.Annotations; - -namespace Microsoft.eShopWeb.PublicApi.CatalogTypeEndpoints; - -public class List : EndpointBaseAsync - .WithoutRequest - .WithActionResult -{ - private readonly IRepository _catalogTypeRepository; - private readonly IMapper _mapper; - - public List(IRepository catalogTypeRepository, - IMapper mapper) - { - _catalogTypeRepository = catalogTypeRepository; - _mapper = mapper; - } - - [HttpGet("api/catalog-types")] - [SwaggerOperation( - Summary = "List Catalog Types", - Description = "List Catalog Types", - OperationId = "catalog-types.List", - Tags = new[] { "CatalogTypeEndpoints" }) - ] - public override async Task> HandleAsync(CancellationToken cancellationToken) - { - var response = new ListCatalogTypesResponse(); - - var items = await _catalogTypeRepository.ListAsync(cancellationToken); - - response.CatalogTypes.AddRange(items.Select(_mapper.Map)); - - return Ok(response); - } -} diff --git a/src/PublicApi/Program.cs b/src/PublicApi/Program.cs index 176d9becd..aeb11bcb5 100644 --- a/src/PublicApi/Program.cs +++ b/src/PublicApi/Program.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Text; -using System.Threading.Tasks; using BlazorShared; using BlazorShared.Models; using MediatR; @@ -9,7 +8,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.eShopWeb.ApplicationCore.Interfaces; @@ -25,20 +23,18 @@ using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using MinimalApi.Endpoint.Configurations.Extensions; +using MinimalApi.Endpoint.Extensions; var builder = WebApplication.CreateBuilder(args); -builder.Logging.AddConsole(); +builder.Services.AddEndpoints(); -// use real database -// Requires LocalDB which can be installed with SQL Server Express 2016 -// https://www.microsoft.com/en-us/download/details.aspx?id=54284 -builder.Services.AddDbContext(c => - c.UseSqlServer(builder.Configuration.GetConnectionString("CatalogConnection"))); +//Use to force loading of appsettings.json of test project +builder.Configuration.AddConfigurationFile(); +builder.Logging.AddConsole(); -// Add Identity DbContext -builder.Services.AddDbContext(options => - options.UseSqlServer(builder.Configuration.GetConnectionString("IdentityConnection"))); +Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); builder.Services.AddIdentity() .AddEntityFrameworkStores() @@ -92,6 +88,7 @@ builder.Services.AddMediatR(typeof(CatalogItem).Assembly); builder.Services.AddAutoMapper(typeof(MappingProfile).Assembly); +builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); @@ -182,5 +179,8 @@ } } +app.MapEndpoints(); app.Logger.LogInformation("LAUNCHING PublicApi"); app.Run(); + +public partial class Program { } diff --git a/src/PublicApi/PublicApi.csproj b/src/PublicApi/PublicApi.csproj index e6500862d..c8e095221 100644 --- a/src/PublicApi/PublicApi.csproj +++ b/src/PublicApi/PublicApi.csproj @@ -13,21 +13,22 @@ + - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Web/Pages/Basket/BasketItemViewModel.cs b/src/Web/Pages/Basket/BasketItemViewModel.cs index b8f65f509..c102dfb9b 100644 --- a/src/Web/Pages/Basket/BasketItemViewModel.cs +++ b/src/Web/Pages/Basket/BasketItemViewModel.cs @@ -6,12 +6,12 @@ public class BasketItemViewModel { public int Id { get; set; } public int CatalogItemId { get; set; } - public string ProductName { get; set; } + public string? ProductName { get; set; } public decimal UnitPrice { get; set; } public decimal OldUnitPrice { get; set; } [Range(0, int.MaxValue, ErrorMessage = "Quantity must be bigger than 0")] public int Quantity { get; set; } - public string PictureUrl { get; set; } + public string? PictureUrl { get; set; } } diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 7e04235fb..2e78ab35b 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Data; @@ -22,15 +21,7 @@ builder.Logging.AddConsole(); -// use real database -// Requires LocalDB which can be installed with SQL Server Express 2016 -// https://www.microsoft.com/en-us/download/details.aspx?id=54284 -builder.Services.AddDbContext(c => - c.UseSqlServer(builder.Configuration.GetConnectionString("CatalogConnection"))); - -// Add Identity DbContext -builder.Services.AddDbContext(options => - options.UseSqlServer(builder.Configuration.GetConnectionString("IdentityConnection"))); +Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); builder.Services.AddCookieSettings(); diff --git a/src/Web/Services/BasketViewModelService.cs b/src/Web/Services/BasketViewModelService.cs index 06cfdf4c6..e07ca618d 100644 --- a/src/Web/Services/BasketViewModelService.cs +++ b/src/Web/Services/BasketViewModelService.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.eShopWeb.ApplicationCore.Entities; +using Microsoft.eShopWeb.ApplicationCore.Entities; using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.ApplicationCore.Specifications; diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index ee60c6526..aac5b0ede 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -20,16 +20,16 @@ - - - + + + - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json index c2bc65927..70989a6a6 100644 --- a/src/Web/appsettings.json +++ b/src/Web/appsettings.json @@ -17,4 +17,4 @@ }, "AllowedHosts": "*" } -} +} \ No newline at end of file diff --git a/tests/FunctionalTests/FunctionalTests.csproj b/tests/FunctionalTests/FunctionalTests.csproj index 2599f180e..1c6a574d4 100644 --- a/tests/FunctionalTests/FunctionalTests.csproj +++ b/tests/FunctionalTests/FunctionalTests.csproj @@ -4,6 +4,8 @@ net6.0 Microsoft.eShopWeb.FunctionalTests false + enable + enable @@ -13,14 +15,14 @@ - + all runtime; build; native; contentfiles; analyzers - + diff --git a/tests/FunctionalTests/PublicApi/ApiTestFixture.cs b/tests/FunctionalTests/PublicApi/ApiTestFixture.cs index 4513284eb..ec8170fd0 100644 --- a/tests/FunctionalTests/PublicApi/ApiTestFixture.cs +++ b/tests/FunctionalTests/PublicApi/ApiTestFixture.cs @@ -9,7 +9,7 @@ namespace Microsoft.eShopWeb.FunctionalTests.PublicApi; -public class TestApiApplication : WebApplicationFactory +public class TestApiApplication : WebApplicationFactory { private readonly string _environment = "Testing"; diff --git a/tests/FunctionalTests/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs b/tests/FunctionalTests/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs index d695245ba..4cec6434b 100644 --- a/tests/FunctionalTests/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs +++ b/tests/FunctionalTests/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs @@ -1,43 +1,43 @@ -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.eShopWeb.ApplicationCore.Constants; -using Microsoft.eShopWeb.FunctionalTests.PublicApi; -using Microsoft.eShopWeb.PublicApi.AuthEndpoints; -using Xunit; +//using System.Net.Http; +//using System.Text; +//using System.Text.Json; +//using System.Threading.Tasks; +//using Microsoft.eShopWeb.ApplicationCore.Constants; +//using Microsoft.eShopWeb.FunctionalTests.PublicApi; +//using Microsoft.eShopWeb.PublicApi.AuthEndpoints; +//using Xunit; -namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; +//namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; -[Collection("Sequential")] -public class AuthenticateEndpoint : IClassFixture -{ - JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; +//[Collection("Sequential")] +//public class AuthenticateEndpoint : IClassFixture +//{ +// JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - public AuthenticateEndpoint(TestApiApplication factory) - { - Client = factory.CreateClient(); - } +// public AuthenticateEndpoint(TestApiApplication factory) +// { +// Client = factory.CreateClient(); +// } - public HttpClient Client { get; } +// public HttpClient Client { get; } - [Theory] - [InlineData("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)] - [InlineData("demouser@microsoft.com", "badpassword", false)] - [InlineData("baduser@microsoft.com", "badpassword", false)] - public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult) - { - var request = new AuthenticateRequest() - { - Username = testUsername, - Password = testPassword - }; - var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); - var response = await Client.PostAsync("api/authenticate", jsonContent); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); +// [Theory] +// [InlineData("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)] +// [InlineData("demouser@microsoft.com", "badpassword", false)] +// [InlineData("baduser@microsoft.com", "badpassword", false)] +// public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult) +// { +// var request = new AuthenticateRequest() +// { +// Username = testUsername, +// Password = testPassword +// }; +// var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); +// var response = await Client.PostAsync("api/authenticate", jsonContent); +// response.EnsureSuccessStatusCode(); +// var stringResponse = await response.Content.ReadAsStringAsync(); +// var model = stringResponse.FromJson(); - Assert.Equal(expectedResult, model.Result); - } -} +// Assert.Equal(expectedResult, model.Result); +// } +//} diff --git a/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/ApiCatalogControllerList.cs b/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/ApiCatalogControllerList.cs deleted file mode 100644 index 1870e731f..000000000 --- a/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/ApiCatalogControllerList.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.eShopWeb.FunctionalTests.PublicApi; -using Microsoft.eShopWeb.Web.ViewModels; -using Xunit; - -namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; - -[Collection("Sequential")] -public class ApiCatalogControllerList : IClassFixture -{ - public ApiCatalogControllerList(TestApiApplication factory) - { - Client = factory.CreateClient(); - } - - public HttpClient Client { get; } - - [Fact] - public async Task ReturnsFirst10CatalogItems() - { - var response = await Client.GetAsync("/api/catalog-items?pageSize=10"); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); - - Assert.Equal(10, model.CatalogItems.Count()); - } - - [Fact] - public async Task ReturnsLast2CatalogItemsGivenPageIndex1() - { - var response = await Client.GetAsync("/api/catalog-items?pageSize=10&pageIndex=1"); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); - - Assert.Equal(2, model.CatalogItems.Count()); - } -} diff --git a/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/CreateEndpoint.cs b/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/CreateEndpoint.cs deleted file mode 100644 index 3b75095ea..000000000 --- a/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/CreateEndpoint.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.eShopWeb.FunctionalTests.PublicApi; -using Microsoft.eShopWeb.FunctionalTests.Web.Api; -using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; -using Xunit; - -namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; - -[Collection("Sequential")] -public class CreateEndpoint : IClassFixture -{ - JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - private int _testBrandId = 1; - private int _testTypeId = 2; - private string _testDescription = "test description"; - private string _testName = "test name"; - private decimal _testPrice = 1.23m; - - public CreateEndpoint(TestApiApplication factory) - { - Client = factory.CreateClient(); - } - - public HttpClient Client { get; } - - [Fact] - public async Task ReturnsNotAuthorizedGivenNormalUserToken() - { - var jsonContent = GetValidNewItemJson(); - var token = ApiTokenHelper.GetNormalUserToken(); - Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var response = await Client.PostAsync("api/catalog-items", jsonContent); - - Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); - } - - [Fact] - public async Task ReturnsSuccessGivenValidNewItemAndAdminUserToken() - { - var jsonContent = GetValidNewItemJson(); - var adminToken = ApiTokenHelper.GetAdminUserToken(); - Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); - var response = await Client.PostAsync("api/catalog-items", jsonContent); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); - - Assert.Equal(_testBrandId, model.CatalogItem.CatalogBrandId); - Assert.Equal(_testTypeId, model.CatalogItem.CatalogTypeId); - Assert.Equal(_testDescription, model.CatalogItem.Description); - Assert.Equal(_testName, model.CatalogItem.Name); - Assert.Equal(_testPrice, model.CatalogItem.Price); - } - - private StringContent GetValidNewItemJson() - { - var request = new CreateCatalogItemRequest() - { - CatalogBrandId = _testBrandId, - CatalogTypeId = _testTypeId, - Description = _testDescription, - Name = _testName, - Price = _testPrice - }; - var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); - - return jsonContent; - } -} diff --git a/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/DeleteEndpoint.cs b/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/DeleteEndpoint.cs deleted file mode 100644 index 5d62e1279..000000000 --- a/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/DeleteEndpoint.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.eShopWeb.FunctionalTests.PublicApi; -using Microsoft.eShopWeb.FunctionalTests.Web.Api; -using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; -using Xunit; - -namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; - -[Collection("Sequential")] -public class DeleteEndpoint : IClassFixture -{ - JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - - public DeleteEndpoint(TestApiApplication factory) - { - Client = factory.CreateClient(); - } - - public HttpClient Client { get; } - - [Fact] - public async Task ReturnsSuccessGivenValidIdAndAdminUserToken() - { - var adminToken = ApiTokenHelper.GetAdminUserToken(); - Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); - var response = await Client.DeleteAsync("api/catalog-items/12"); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); - - Assert.Equal("Deleted", model.Status); - } - - [Fact] - public async Task ReturnsNotFoundGivenInvalidIdAndAdminUserToken() - { - var adminToken = ApiTokenHelper.GetAdminUserToken(); - Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); - var response = await Client.DeleteAsync("api/catalog-items/0"); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } -} diff --git a/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/GetByIdEndpoint.cs b/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/GetByIdEndpoint.cs deleted file mode 100644 index 0d7e1b436..000000000 --- a/tests/FunctionalTests/PublicApi/CatalogItemEndpoints/GetByIdEndpoint.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Net; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.eShopWeb.FunctionalTests.PublicApi; -using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; -using Xunit; - -namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers; - -[Collection("Sequential")] -public class GetByIdEndpoint : IClassFixture -{ - JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - - public GetByIdEndpoint(TestApiApplication factory) - { - Client = factory.CreateClient(); - } - - public HttpClient Client { get; } - - [Fact] - public async Task ReturnsItemGivenValidId() - { - var response = await Client.GetAsync("api/catalog-items/5"); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); - - Assert.Equal(5, model.CatalogItem.Id); - Assert.Equal("Roslyn Red Sheet", model.CatalogItem.Name); - } - - [Fact] - public async Task ReturnsNotFoundGivenInvalidId() - { - var response = await Client.GetAsync("api/catalog-items/0"); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } -} diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj index 9512dee47..98dd06e42 100644 --- a/tests/IntegrationTests/IntegrationTests.csproj +++ b/tests/IntegrationTests/IntegrationTests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/PublicApiIntegrationTests/ApiTokenHelper.cs b/tests/PublicApiIntegrationTests/ApiTokenHelper.cs new file mode 100644 index 000000000..75fd1b1a8 --- /dev/null +++ b/tests/PublicApiIntegrationTests/ApiTokenHelper.cs @@ -0,0 +1,50 @@ +using Microsoft.eShopWeb.ApplicationCore.Constants; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace PublicApiIntegrationTests +{ + public class ApiTokenHelper + { + public static string GetAdminUserToken() + { + string userName = "admin@microsoft.com"; + string[] roles = { "Administrators" }; + + return CreateToken(userName, roles); + } + + public static string GetNormalUserToken() + { + string userName = "demouser@microsoft.com"; + string[] roles = { }; + + return CreateToken(userName, roles); + } + + private static string CreateToken(string userName, string[] roles) + { + var claims = new List { new Claim(ClaimTypes.Name, userName) }; + + foreach (var role in roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims.ToArray()), + Expires = DateTime.UtcNow.AddHours(1), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + var tokenHandler = new JwtSecurityTokenHandler(); + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + } +} diff --git a/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs b/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs new file mode 100644 index 000000000..3d2aa93fe --- /dev/null +++ b/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.eShopWeb; +using Microsoft.eShopWeb.ApplicationCore.Constants; +using Microsoft.eShopWeb.PublicApi.AuthEndpoints; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace PublicApiIntegrationTests.AuthEndpoints +{ + [TestClass] + public class AuthenticateEndpoint + { + [TestMethod] + [DataRow("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)] + [DataRow("demouser@microsoft.com", "badpassword", false)] + [DataRow("baduser@microsoft.com", "badpassword", false)] + public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult) + { + var request = new AuthenticateRequest() + { + Username = testUsername, + Password = testPassword + }; + var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); + var response = await ProgramTest.NewClient.PostAsync("api/authenticate", jsonContent); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + + Assert.AreEqual(expectedResult, model.Result); + } + } +} \ No newline at end of file diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs new file mode 100644 index 000000000..5882db04b --- /dev/null +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs @@ -0,0 +1,32 @@ +using Microsoft.eShopWeb; +using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net; +using System.Threading.Tasks; + +namespace PublicApiIntegrationTests.CatalogItemEndpoints +{ + [TestClass] + public class CatalogItemGetByIdEndpointTest + { + [TestMethod] + public async Task ReturnsItemGivenValidId() + { + var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/5"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + + Assert.AreEqual(5, model.CatalogItem.Id); + Assert.AreEqual("Roslyn Red Sheet", model.CatalogItem.Name); + } + + [TestMethod] + public async Task ReturnsNotFoundGivenInvalidId() + { + var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/0"); + + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + } + } +} diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs new file mode 100644 index 000000000..1040463c2 --- /dev/null +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs @@ -0,0 +1,49 @@ +using Microsoft.eShopWeb; +using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints; +using Microsoft.eShopWeb.Web.ViewModels; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; +using System.Threading.Tasks; + +namespace PublicApiIntegrationTests.CatalogItemEndpoints +{ + [TestClass] + public class CatalogItemListPagedEndpoint + { + [TestMethod] + public async Task ReturnsFirst10CatalogItems() + { + var client = ProgramTest.NewClient; + var response = await client.GetAsync("/api/catalog-items?pageSize=10"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + + Assert.AreEqual(10, model.CatalogItems.Count()); + } + + [TestMethod] + public async Task ReturnsCorrectCatalogItemsGivenPageIndex1() + { + + var pageSize = 10; + var pageIndex = 1; + + var client = ProgramTest.NewClient; + var response = await client.GetAsync($"/api/catalog-items"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + var totalItem = model.CatalogItems.Count(); + + var response2 = await client.GetAsync($"/api/catalog-items?pageSize={pageSize}&pageIndex={pageIndex}"); + response.EnsureSuccessStatusCode(); + var stringResponse2 = await response2.Content.ReadAsStringAsync(); + var model2 = stringResponse2.FromJson(); + + var totalExpected = totalItem - (pageSize * pageIndex); + + Assert.AreEqual(totalExpected, model2.CatalogItems.Count()); + } + } +} diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs new file mode 100644 index 000000000..a85923d04 --- /dev/null +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs @@ -0,0 +1,69 @@ +using BlazorShared.Models; +using Microsoft.eShopWeb; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace PublicApiIntegrationTests.AuthEndpoints +{ + [TestClass] + public class CreateCatalogItemEndpointTest + { + private int _testBrandId = 1; + private int _testTypeId = 2; + private string _testDescription = "test description"; + private string _testName = "test name"; + private decimal _testPrice = 1.23m; + + + [TestMethod] + public async Task ReturnsNotAuthorizedGivenNormalUserToken() + { + var jsonContent = GetValidNewItemJson(); + var token = ApiTokenHelper.GetNormalUserToken(); + var client = ProgramTest.NewClient; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var response = await client.PostAsync("api/catalog-items", jsonContent); + + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } + + [TestMethod] + public async Task ReturnsSuccessGivenValidNewItemAndAdminUserToken() + { + var jsonContent = GetValidNewItemJson(); + var adminToken = ApiTokenHelper.GetAdminUserToken(); + var client = ProgramTest.NewClient; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); + var response = await client.PostAsync("api/catalog-items", jsonContent); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + + Assert.AreEqual(_testBrandId, model.CatalogItem.CatalogBrandId); + Assert.AreEqual(_testTypeId, model.CatalogItem.CatalogTypeId); + Assert.AreEqual(_testDescription, model.CatalogItem.Description); + Assert.AreEqual(_testName, model.CatalogItem.Name); + Assert.AreEqual(_testPrice, model.CatalogItem.Price); + } + + private StringContent GetValidNewItemJson() + { + var request = new CreateCatalogItemRequest() + { + CatalogBrandId = _testBrandId, + CatalogTypeId = _testTypeId, + Description = _testDescription, + Name = _testName, + Price = _testPrice + }; + var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); + + return jsonContent; + } + } +} diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs new file mode 100644 index 000000000..f41976ec3 --- /dev/null +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs @@ -0,0 +1,38 @@ +using BlazorShared.Models; +using Microsoft.eShopWeb; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace PublicApiIntegrationTests.CatalogItemEndpoints +{ + [TestClass] + public class DeleteCatalogItemEndpointTest + { + [TestMethod] + public async Task ReturnsSuccessGivenValidIdAndAdminUserToken() + { + var adminToken = ApiTokenHelper.GetAdminUserToken(); + var client = ProgramTest.NewClient; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); + var response = await client.DeleteAsync("api/catalog-items/12"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + + Assert.AreEqual("Deleted", model.Status); + } + + [TestMethod] + public async Task ReturnsNotFoundGivenInvalidIdAndAdminUserToken() + { + var adminToken = ApiTokenHelper.GetAdminUserToken(); + var client = ProgramTest.NewClient; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); + var response = await client.DeleteAsync("api/catalog-items/0"); + + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); + } + } +} diff --git a/tests/PublicApiIntegrationTests/ProgramTest.cs b/tests/PublicApiIntegrationTests/ProgramTest.cs new file mode 100644 index 000000000..ca922346f --- /dev/null +++ b/tests/PublicApiIntegrationTests/ProgramTest.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Net.Http; + +namespace PublicApiIntegrationTests +{ + [TestClass] + public class ProgramTest + { + private static WebApplicationFactory _application; + + public static HttpClient NewClient + { + get + { + return _application.CreateClient(); + } + } + + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext _) + { + _application = new WebApplicationFactory(); + + } + } +} diff --git a/tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj b/tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj new file mode 100644 index 000000000..1b3a9e667 --- /dev/null +++ b/tests/PublicApiIntegrationTests/PublicApiIntegrationTests.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + false + + + + + + + + + Always + true + PreserveNewest + + + + + + + + + + + + + + + + + diff --git a/tests/PublicApiIntegrationTests/appsettings.json b/tests/PublicApiIntegrationTests/appsettings.json new file mode 100644 index 000000000..417e68b41 --- /dev/null +++ b/tests/PublicApiIntegrationTests/appsettings.json @@ -0,0 +1,3 @@ +{ + "UseOnlyInMemoryDatabase": true +}