Adding Endpoints with Authorization in separate PublicApi project (#413)
* Adding tests for GetById endpoint * Updating tests and messages * Adding paged endpoint and also AutoMapper * Authenticate endpoint works as bool with tests * Got JWT token security working with Create and Delete endpoints and Swashbuckle. * Working on getting cookie and jwt token auth working in the same app All tests are passing * Creating new project and moving APIs Build succeeds; tests need updated. * all tests passing after moving services to PublicApi project * Fix authorize attributes * Uncomment and update ApiCatalogControllerLists tests Co-authored-by: Eric Fleming <eric-fleming18@hotmail.com>
This commit is contained in:
@@ -30,12 +30,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FCBE191-34FE-4B2E-8915-CA81553958AD}"
|
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FCBE191-34FE-4B2E-8915-CA81553958AD}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApi", "src\PublicApi\PublicApi.csproj", "{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7C461394-ABDC-43CD-A798-71249C58BA67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7C461394-ABDC-43CD-A798-71249C58BA67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7C461394-ABDC-43CD-A798-71249C58BA67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@@ -56,25 +62,26 @@ Global
|
|||||||
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{227CF035-29B0-448D-97E4-944F9EA850E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{227CF035-29B0-448D-97E4-944F9EA850E5}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1FCBE191-34FE-4B2E-8915-CA81553958AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{227CF035-29B0-448D-97E4-944F9EA850E5} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
||||||
{7C461394-ABDC-43CD-A798-71249C58BA67} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
{7C461394-ABDC-43CD-A798-71249C58BA67} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
||||||
{7FED7440-2311-4D1E-958B-3E887C585CD2} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
{7FED7440-2311-4D1E-958B-3E887C585CD2} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
||||||
{EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
|
{EF6877E6-59CB-43A7-8C2C-E70DD70CC5B6} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
|
||||||
{0F576306-7E2D-49B7-87B1-EB5D94CFD5FC} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
|
{0F576306-7E2D-49B7-87B1-EB5D94CFD5FC} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
|
||||||
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
|
{7EFB5482-F942-4C3D-94B0-9B70596E6D0A} = {15EA4737-125B-4E6E-A806-E13B7EBCDCCF}
|
||||||
{227CF035-29B0-448D-97E4-944F9EA850E5} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
{B5E4F33C-4667-4A55-AF6A-740F84C4CF3A} = {419A6ACE-0419-4315-A6FB-B0E63D39432E}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B}
|
SolutionGuid = {49813262-5DA3-4D61-ABD3-493C74CE8C2B}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
public const string ADMINISTRATORS = "Administrators";
|
public const string ADMINISTRATORS = "Administrators";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Don't use this in production
|
||||||
public const string DEFAULT_PASSWORD = "Pass@word1";
|
public const string DEFAULT_PASSWORD = "Pass@word1";
|
||||||
|
|
||||||
|
// TODO: Change this to an environment variable
|
||||||
|
public const string JWT_SECRET_KEY = "SecretKeyOfDoomThatMustBeAMinimumNumberOfBytes";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/ApplicationCore/Interfaces/ITokenClaimsService.cs
Normal file
9
src/ApplicationCore/Interfaces/ITokenClaimsService.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.ApplicationCore.Interfaces
|
||||||
|
{
|
||||||
|
public interface ITokenClaimsService
|
||||||
|
{
|
||||||
|
Task<string> GetTokenAsync(string userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Infrastructure/Identity/IdentityTokenClaimService.cs
Normal file
46
src/Infrastructure/Identity/IdentityTokenClaimService.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.Infrastructure.Identity
|
||||||
|
{
|
||||||
|
public class IdentityTokenClaimService : ITokenClaimsService
|
||||||
|
{
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
|
||||||
|
public IdentityTokenClaimService(UserManager<ApplicationUser> userManager)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetTokenAsync(string userName)
|
||||||
|
{
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY);
|
||||||
|
var user = await _userManager.FindByNameAsync(userName);
|
||||||
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
|
var claims = new List<Claim> { new Claim(ClaimTypes.Name, userName) };
|
||||||
|
|
||||||
|
foreach(var role in roles)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, role));
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity(claims.ToArray()),
|
||||||
|
Expires = DateTime.UtcNow.AddDays(7),
|
||||||
|
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
||||||
|
};
|
||||||
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
return tokenHandler.WriteToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />
|
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
|
||||||
|
{
|
||||||
|
public class AuthenticateRequest : BaseRequest
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
|
||||||
|
{
|
||||||
|
public class AuthenticateResponse : BaseResponse
|
||||||
|
{
|
||||||
|
public AuthenticateResponse(Guid correlationId) : base(correlationId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthenticateResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Result { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/PublicApi/AuthEndpoints/Authenticate.cs
Normal file
50
src/PublicApi/AuthEndpoints/Authenticate.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using Ardalis.ApiEndpoints;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Identity;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using System;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints
|
||||||
|
{
|
||||||
|
public class Authenticate : BaseAsyncEndpoint<AuthenticateRequest, AuthenticateResponse>
|
||||||
|
{
|
||||||
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
private readonly ITokenClaimsService _tokenClaimsService;
|
||||||
|
|
||||||
|
public Authenticate(SignInManager<ApplicationUser> signInManager,
|
||||||
|
ITokenClaimsService tokenClaimsService)
|
||||||
|
{
|
||||||
|
_signInManager = signInManager;
|
||||||
|
_tokenClaimsService = tokenClaimsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("api/authenticate")]
|
||||||
|
[SwaggerOperation(
|
||||||
|
Summary = "Authenticates a user",
|
||||||
|
Description = "Authenticates a user",
|
||||||
|
OperationId = "auth.authenticate",
|
||||||
|
Tags = new[] { "AuthEndpoints" })
|
||||||
|
]
|
||||||
|
public override async Task<ActionResult<AuthenticateResponse>> HandleAsync(AuthenticateRequest request)
|
||||||
|
{
|
||||||
|
var response = new AuthenticateResponse(request.CorrelationId());
|
||||||
|
|
||||||
|
var result = await _signInManager.PasswordSignInAsync(request.Username, request.Password, false, true);
|
||||||
|
|
||||||
|
response.Result = result.Succeeded;
|
||||||
|
|
||||||
|
response.Token = await _tokenClaimsService.GetTokenAsync(request.Username);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API
|
namespace Microsoft.eShopWeb.PublicApi
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class used by API requests
|
/// Base class used by API requests
|
||||||
@@ -13,8 +13,4 @@ namespace Microsoft.eShopWeb.Web.API
|
|||||||
protected Guid _correlationId = Guid.NewGuid();
|
protected Guid _correlationId = Guid.NewGuid();
|
||||||
public Guid CorrelationId() => _correlationId;
|
public Guid CorrelationId() => _correlationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class BaseRequest : BaseMessage
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
9
src/PublicApi/BaseRequest.cs
Normal file
9
src/PublicApi/BaseRequest.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Microsoft.eShopWeb.PublicApi
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class used by API requests
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseRequest : BaseMessage
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API
|
namespace Microsoft.eShopWeb.PublicApi
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class used by API responses
|
/// Base class used by API responses
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
public class CatalogItemDto
|
public class CatalogItemDto
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
public class CreateCatalogItemRequest : BaseRequest
|
public class CreateCatalogItemRequest : BaseRequest
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
public class CreateCatalogItemResponse : BaseResponse
|
public class CreateCatalogItemResponse : BaseResponse
|
||||||
{
|
{
|
||||||
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CreateCatalogItemResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public CatalogItemDto CatalogItem { get; set; }
|
public CatalogItemDto CatalogItem { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
using Ardalis.ApiEndpoints;
|
using Ardalis.ApiEndpoints;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||||
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
||||||
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
|
[Authorize(Roles = AuthorizationConstants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
public class Create : BaseAsyncEndpoint<CreateCatalogItemRequest, CreateCatalogItemResponse>
|
public class Create : BaseAsyncEndpoint<CreateCatalogItemRequest, CreateCatalogItemResponse>
|
||||||
{
|
{
|
||||||
private readonly IAsyncRepository<CatalogItem> _itemRepository;
|
private readonly IAsyncRepository<CatalogItem> _itemRepository;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
public class DeleteCatalogItemRequest : BaseRequest
|
public class DeleteCatalogItemRequest : BaseRequest
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
public class DeleteCatalogItemResponse : BaseResponse
|
public class DeleteCatalogItemResponse : BaseResponse
|
||||||
{
|
{
|
||||||
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DeleteCatalogItemResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public string Status { get; set; } = "Deleted";
|
public string Status { get; set; } = "Deleted";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
using Ardalis.ApiEndpoints;
|
using Ardalis.ApiEndpoints;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||||
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
||||||
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
|
[Authorize(Roles = AuthorizationConstants.Roles.ADMINISTRATORS, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
public class Delete : BaseAsyncEndpoint<DeleteCatalogItemRequest, DeleteCatalogItemResponse>
|
public class Delete : BaseAsyncEndpoint<DeleteCatalogItemRequest, DeleteCatalogItemResponse>
|
||||||
{
|
{
|
||||||
private readonly IAsyncRepository<CatalogItem> _itemRepository;
|
private readonly IAsyncRepository<CatalogItem> _itemRepository;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
public class GetByIdCatalogItemRequest : BaseRequest
|
public class GetByIdCatalogItemRequest : BaseRequest
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
public class GetByIdCatalogItemResponse : BaseResponse
|
public class GetByIdCatalogItemResponse : BaseResponse
|
||||||
{
|
{
|
||||||
@@ -8,6 +8,10 @@ namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GetByIdCatalogItemResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public CatalogItemDto CatalogItem { get; set; }
|
public CatalogItemDto CatalogItem { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
|||||||
using Swashbuckle.AspNetCore.Annotations;
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API.CatalogItemEndpoints
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
{
|
{
|
||||||
public class GetById : BaseAsyncEndpoint<GetByIdCatalogItemRequest, GetByIdCatalogItemResponse>
|
public class GetById : BaseAsyncEndpoint<GetByIdCatalogItemRequest, GetByIdCatalogItemResponse>
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
|
{
|
||||||
|
public class ListPagedCatalogItemResponse : BaseResponse
|
||||||
|
{
|
||||||
|
public ListPagedCatalogItemResponse(Guid correlationId) : base(correlationId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListPagedCatalogItemResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CatalogItemDto> CatalogItems { get; set; } = new List<CatalogItemDto>();
|
||||||
|
public int PageCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/PublicApi/CatalogItemEndpoints/ListPaged.cs
Normal file
61
src/PublicApi/CatalogItemEndpoints/ListPaged.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using Ardalis.ApiEndpoints;
|
||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Specifications;
|
||||||
|
using Swashbuckle.AspNetCore.Annotations;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints
|
||||||
|
{
|
||||||
|
public class ListPaged : BaseAsyncEndpoint<ListPagedCatalogItemRequest, ListPagedCatalogItemResponse>
|
||||||
|
{
|
||||||
|
private readonly IAsyncRepository<CatalogItem> _itemRepository;
|
||||||
|
private readonly IUriComposer _uriComposer;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public ListPaged(IAsyncRepository<CatalogItem> itemRepository,
|
||||||
|
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<ActionResult<ListPagedCatalogItemResponse>> HandleAsync([FromQuery]ListPagedCatalogItemRequest request)
|
||||||
|
{
|
||||||
|
var response = new ListPagedCatalogItemResponse(request.CorrelationId());
|
||||||
|
|
||||||
|
var filterSpec = new CatalogFilterSpecification(request.CatalogBrandId, request.CatalogTypeId);
|
||||||
|
int totalItems = await _itemRepository.CountAsync(filterSpec);
|
||||||
|
|
||||||
|
var pagedSpec = new CatalogFilterPaginatedSpecification(
|
||||||
|
skip: request.PageIndex * request.PageSize,
|
||||||
|
take: request.PageSize,
|
||||||
|
brandId: request.CatalogBrandId,
|
||||||
|
typeId: request.CatalogTypeId);
|
||||||
|
|
||||||
|
var items = await _itemRepository.ListAsync(pagedSpec);
|
||||||
|
|
||||||
|
response.CatalogItems.AddRange(items.Select(_mapper.Map<CatalogItemDto>));
|
||||||
|
foreach (CatalogItemDto item in response.CatalogItems)
|
||||||
|
{
|
||||||
|
item.PictureUri = _uriComposer.ComposePicUri(item.PictureUri);
|
||||||
|
}
|
||||||
|
response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize).ToString());
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.API
|
namespace Microsoft.eShopWeb.PublicApi
|
||||||
{
|
{
|
||||||
public class CustomSchemaFilters : ISchemaFilter
|
public class CustomSchemaFilters : ISchemaFilter
|
||||||
{
|
{
|
||||||
22
src/PublicApi/Dockerfile
Normal file
22
src/PublicApi/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 80
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["src/PublicApi/PublicApi.csproj", "src/PublicApi/"]
|
||||||
|
RUN dotnet restore "src/PublicApi/PublicApi.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/src/PublicApi"
|
||||||
|
RUN dotnet build "PublicApi.csproj" -c Release -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "PublicApi.csproj" -c Release -o /app/publish
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "PublicApi.dll"]
|
||||||
14
src/PublicApi/MappingProfile.cs
Normal file
14
src/PublicApi/MappingProfile.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Entities;
|
||||||
|
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.PublicApi
|
||||||
|
{
|
||||||
|
public class MappingProfile : Profile
|
||||||
|
{
|
||||||
|
public MappingProfile()
|
||||||
|
{
|
||||||
|
CreateMap<CatalogItem, CatalogItemDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/PublicApi/Program.cs
Normal file
51
src/PublicApi/Program.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Data;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Identity;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.PublicApi
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public async static Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var host = CreateHostBuilder(args)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
using (var scope = host.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var catalogContext = services.GetRequiredService<CatalogContext>();
|
||||||
|
await CatalogContextSeed.SeedAsync(catalogContext, loggerFactory);
|
||||||
|
|
||||||
|
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
|
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
|
||||||
|
await AppIdentityDbContextSeed.SeedAsync(userManager, roleManager);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var logger = loggerFactory.CreateLogger<Program>();
|
||||||
|
logger.LogError(ex, "An error occurred seeding the DB.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host.Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
|
Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder.UseStartup<Startup>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/PublicApi/Properties/launchSettings.json
Normal file
37
src/PublicApi/Properties/launchSettings.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:52023",
|
||||||
|
"sslPort": 44339
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PublicApi": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||||
|
},
|
||||||
|
"Docker": {
|
||||||
|
"commandName": "Docker",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
|
||||||
|
"publishAllPorts": true,
|
||||||
|
"useSSL": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/PublicApi/PublicApi.csproj
Normal file
39
src/PublicApi/PublicApi.csproj
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<RootNamespace>Microsoft.eShopWeb.PublicApi</RootNamespace>
|
||||||
|
<UserSecretsId>5b662463-1efd-4bae-bde4-befe0be3e8ff</UserSecretsId>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<DockerfileContext>..\..</DockerfileContext>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Ardalis.ApiEndpoints" Version="1.0.0" />
|
||||||
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.5.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.5.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.5" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
|
||||||
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" />
|
||||||
|
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />
|
||||||
|
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
</Project>
|
||||||
193
src/PublicApi/Startup.cs
Normal file
193
src/PublicApi/Startup.cs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
|
||||||
|
using Microsoft.eShopWeb.ApplicationCore.Services;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Data;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Identity;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Logging;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.PublicApi
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
public void ConfigureDevelopmentServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
// use in-memory database
|
||||||
|
ConfigureInMemoryDatabases(services);
|
||||||
|
|
||||||
|
// use real database
|
||||||
|
//ConfigureProductionServices(services);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureInMemoryDatabases(IServiceCollection services)
|
||||||
|
{
|
||||||
|
// use in-memory database
|
||||||
|
services.AddDbContext<CatalogContext>(c =>
|
||||||
|
c.UseInMemoryDatabase("Catalog"));
|
||||||
|
|
||||||
|
// Add Identity DbContext
|
||||||
|
services.AddDbContext<AppIdentityDbContext>(options =>
|
||||||
|
options.UseInMemoryDatabase("Identity"));
|
||||||
|
|
||||||
|
ConfigureServices(services);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureProductionServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
// 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<CatalogContext>(c =>
|
||||||
|
c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection")));
|
||||||
|
|
||||||
|
// Add Identity DbContext
|
||||||
|
services.AddDbContext<AppIdentityDbContext>(options =>
|
||||||
|
options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));
|
||||||
|
|
||||||
|
ConfigureServices(services);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureTestingServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
ConfigureInMemoryDatabases(services);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||||
|
.AddEntityFrameworkStores<AppIdentityDbContext>()
|
||||||
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>));
|
||||||
|
services.Configure<CatalogSettings>(Configuration);
|
||||||
|
services.AddSingleton<IUriComposer>(new UriComposer(Configuration.Get<CatalogSettings>()));
|
||||||
|
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
|
||||||
|
services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
|
||||||
|
|
||||||
|
// Add memory cache services
|
||||||
|
services.AddMemoryCache();
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/46938248/asp-net-core-2-0-combining-cookies-and-bearer-authorization-for-the-same-endpoin
|
||||||
|
var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY);
|
||||||
|
services.AddAuthentication(config =>
|
||||||
|
{
|
||||||
|
//config.DefaultScheme = "smart";
|
||||||
|
//config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
//config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
|
||||||
|
config.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
config.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddJwtBearer(x =>
|
||||||
|
{
|
||||||
|
x.RequireHttpsMetadata = false;
|
||||||
|
x.SaveToken = true;
|
||||||
|
x.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||||
|
ValidateIssuer = false,
|
||||||
|
ValidateAudience = false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
services.AddControllers();
|
||||||
|
|
||||||
|
services.AddAutoMapper(typeof(Startup).Assembly);
|
||||||
|
|
||||||
|
services.AddSwaggerGen(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
|
||||||
|
c.EnableAnnotations();
|
||||||
|
c.SchemaFilter<CustomSchemaFilters>();
|
||||||
|
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
|
||||||
|
Enter 'Bearer' [space] and then your token in the text input below.
|
||||||
|
\r\n\r\nExample: 'Bearer 12345abcdef'",
|
||||||
|
Name = "Authorization",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Type = SecuritySchemeType.ApiKey,
|
||||||
|
Scheme = "Bearer"
|
||||||
|
});
|
||||||
|
|
||||||
|
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Reference = new OpenApiReference
|
||||||
|
{
|
||||||
|
Type = ReferenceType.SecurityScheme,
|
||||||
|
Id = "Bearer"
|
||||||
|
},
|
||||||
|
Scheme = "oauth2",
|
||||||
|
Name = "Bearer",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
|
||||||
|
},
|
||||||
|
new List<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// Enable middleware to serve generated Swagger as a JSON endpoint.
|
||||||
|
app.UseSwagger();
|
||||||
|
|
||||||
|
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
|
||||||
|
// specifying the Swagger JSON endpoint.
|
||||||
|
app.UseSwaggerUI(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapControllers();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/PublicApi/appsettings.Development.json
Normal file
9
src/PublicApi/appsettings.Development.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/PublicApi/appsettings.json
Normal file
16
src/PublicApi/appsettings.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;",
|
||||||
|
"IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;"
|
||||||
|
},
|
||||||
|
"CatalogBaseUrl": "",
|
||||||
|
"Logging": {
|
||||||
|
"IncludeScopes": false,
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Warning",
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"System": "Warning"
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AutoMapper.Configuration.Annotations;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -9,6 +10,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
|
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
|
||||||
{
|
{
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
[IgnoreAntiforgeryToken]
|
||||||
public class LogoutModel : PageModel
|
public class LogoutModel : PageModel
|
||||||
{
|
{
|
||||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.Controllers.Api
|
namespace Microsoft.eShopWeb.Web.Controllers.Api
|
||||||
{
|
{
|
||||||
|
// No longer used - shown for reference only if using full controllers instead of Endpoints for APIs
|
||||||
[Route("api/[controller]/[action]")]
|
[Route("api/[controller]/[action]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class BaseApiController : ControllerBase
|
public class BaseApiController : ControllerBase
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
using Microsoft.eShopWeb.Web.Services;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.Web.Controllers.Api
|
|
||||||
{
|
|
||||||
public class CatalogController : BaseApiController
|
|
||||||
{
|
|
||||||
private readonly ICatalogViewModelService _catalogViewModelService;
|
|
||||||
|
|
||||||
public CatalogController(ICatalogViewModelService catalogViewModelService) => _catalogViewModelService = catalogViewModelService;
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> List(int? brandFilterApplied, int? typesFilterApplied, int? page)
|
|
||||||
{
|
|
||||||
var itemsPage = 10;
|
|
||||||
var catalogModel = await _catalogViewModelService.GetCatalogItems(page ?? 0, itemsPage, brandFilterApplied, typesFilterApplied);
|
|
||||||
return Ok(catalogModel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.eShopWeb.Web.Features.MyOrders;
|
using Microsoft.eShopWeb.Web.Features.MyOrders;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.eShopWeb.Infrastructure.Data;
|
using Microsoft.eShopWeb.Infrastructure.Data;
|
||||||
using Microsoft.eShopWeb.Infrastructure.Identity;
|
using Microsoft.eShopWeb.Infrastructure.Identity;
|
||||||
using Microsoft.eShopWeb.Web.API;
|
using Microsoft.eShopWeb.Infrastructure.Logging;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Services;
|
||||||
|
using Microsoft.eShopWeb.Web.Interfaces;
|
||||||
|
using Microsoft.eShopWeb.Web.Services;
|
||||||
using Microsoft.eShopWeb.Web.Configuration;
|
using Microsoft.eShopWeb.Web.Configuration;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -107,12 +110,6 @@ namespace Microsoft.eShopWeb.Web
|
|||||||
});
|
});
|
||||||
services.AddControllersWithViews();
|
services.AddControllersWithViews();
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
services.AddSwaggerGen(c =>
|
|
||||||
{
|
|
||||||
c.SwaggerDoc("v1", new OpenApi.Models.OpenApiInfo { Title = "My API", Version = "v1" });
|
|
||||||
c.EnableAnnotations();
|
|
||||||
c.SchemaFilter<CustomSchemaFilters>();
|
|
||||||
});
|
|
||||||
services.AddHealthChecks();
|
services.AddHealthChecks();
|
||||||
services.Configure<ServiceConfig>(config =>
|
services.Configure<ServiceConfig>(config =>
|
||||||
{
|
{
|
||||||
@@ -166,15 +163,6 @@ namespace Microsoft.eShopWeb.Web
|
|||||||
app.UseCookiePolicy();
|
app.UseCookiePolicy();
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
// Enable middleware to serve generated Swagger as a JSON endpoint.
|
|
||||||
app.UseSwagger();
|
|
||||||
|
|
||||||
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
|
|
||||||
// specifying the Swagger JSON endpoint.
|
|
||||||
app.UseSwaggerUI(c =>
|
|
||||||
{
|
|
||||||
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
|
|
||||||
});
|
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<PackageReference Include="Ardalis.ApiEndpoints" Version="1.0.0" />
|
<PackageReference Include="Ardalis.ApiEndpoints" Version="1.0.0" />
|
||||||
<PackageReference Include="Ardalis.ListStartupServices" Version="1.1.3" />
|
<PackageReference Include="Ardalis.ListStartupServices" Version="1.1.3" />
|
||||||
<PackageReference Include="Ardalis.Specification" Version="3.0.0" />
|
<PackageReference Include="Ardalis.Specification" Version="3.0.0" />
|
||||||
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
|
||||||
|
|
||||||
<PackageReference Include="MediatR" Version="8.0.1" />
|
<PackageReference Include="MediatR" Version="8.0.1" />
|
||||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="8.0.0" />
|
||||||
@@ -30,17 +31,17 @@
|
|||||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.6.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.6.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.5" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" />
|
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.0" />
|
<PackageReference Include="Microsoft.Web.LibraryManager.Build" Version="2.1.76" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.5.0" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.5.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="3.1.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.5" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="wwwroot\fonts\" />
|
<Folder Include="wwwroot\fonts\" />
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" />
|
<ProjectReference Include="..\..\src\ApplicationCore\ApplicationCore.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\PublicApi\PublicApi.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Web\Web.csproj" />
|
<ProjectReference Include="..\..\src\Web\Web.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -33,8 +34,4 @@
|
|||||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Web\ApiEndpoints\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
79
tests/FunctionalTests/PublicApi/ApiTestFixture.cs
Normal file
79
tests/FunctionalTests/PublicApi/ApiTestFixture.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Data;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Identity;
|
||||||
|
using Microsoft.eShopWeb.PublicApi;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.FunctionalTests.PublicApi
|
||||||
|
{
|
||||||
|
public class ApiTestFixture : WebApplicationFactory<Startup>
|
||||||
|
{
|
||||||
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
|
{
|
||||||
|
builder.UseEnvironment("Testing");
|
||||||
|
|
||||||
|
builder.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddEntityFrameworkInMemoryDatabase();
|
||||||
|
|
||||||
|
// Create a new service provider.
|
||||||
|
var provider = services
|
||||||
|
.AddEntityFrameworkInMemoryDatabase()
|
||||||
|
.BuildServiceProvider();
|
||||||
|
|
||||||
|
// Add a database context (ApplicationDbContext) using an in-memory
|
||||||
|
// database for testing.
|
||||||
|
services.AddDbContext<CatalogContext>(options =>
|
||||||
|
{
|
||||||
|
options.UseInMemoryDatabase("InMemoryDbForTesting");
|
||||||
|
options.UseInternalServiceProvider(provider);
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddDbContext<AppIdentityDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.UseInMemoryDatabase("Identity");
|
||||||
|
options.UseInternalServiceProvider(provider);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build the service provider.
|
||||||
|
var sp = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// Create a scope to obtain a reference to the database
|
||||||
|
// context (ApplicationDbContext).
|
||||||
|
using (var scope = sp.CreateScope())
|
||||||
|
{
|
||||||
|
var scopedServices = scope.ServiceProvider;
|
||||||
|
var db = scopedServices.GetRequiredService<CatalogContext>();
|
||||||
|
var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();
|
||||||
|
|
||||||
|
var logger = scopedServices
|
||||||
|
.GetRequiredService<ILogger<ApiTestFixture>>();
|
||||||
|
|
||||||
|
// Ensure the database is created.
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Seed the database with test data.
|
||||||
|
CatalogContextSeed.SeedAsync(db, loggerFactory).Wait();
|
||||||
|
|
||||||
|
// seed sample user data
|
||||||
|
var userManager = scopedServices.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
|
var roleManager = scopedServices.GetRequiredService<RoleManager<IdentityRole>>();
|
||||||
|
AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, $"An error occurred seeding the " +
|
||||||
|
"database with test messages. Error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
tests/FunctionalTests/PublicApi/ApiTokenHelper.cs
Normal file
51
tests/FunctionalTests/PublicApi/ApiTokenHelper.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||||
|
using Microsoft.eShopWeb.Infrastructure.Identity;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.FunctionalTests.Web.Api
|
||||||
|
{
|
||||||
|
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<Claim> { 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.eShopWeb.ApplicationCore.Constants;
|
||||||
|
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
|
||||||
|
using Microsoft.eShopWeb.PublicApi.AuthEndpoints;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
||||||
|
{
|
||||||
|
[Collection("Sequential")]
|
||||||
|
public class AuthenticateEndpoint : IClassFixture<ApiTestFixture>
|
||||||
|
{
|
||||||
|
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
|
public AuthenticateEndpoint(ApiTestFixture factory)
|
||||||
|
{
|
||||||
|
Client = factory.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpClient Client { get; }
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)]
|
||||||
|
[InlineData("demouser@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<AuthenticateResponse>();
|
||||||
|
|
||||||
|
Assert.Equal(expectedResult, model.Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
using Microsoft.eShopWeb.Web.ViewModels;
|
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
|
||||||
|
using Microsoft.eShopWeb.Web.ViewModels;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
||||||
{
|
{
|
||||||
[Collection("Sequential")]
|
[Collection("Sequential")]
|
||||||
public class ApiCatalogControllerList : IClassFixture<WebTestFixture>
|
public class ApiCatalogControllerList : IClassFixture<ApiTestFixture>
|
||||||
{
|
{
|
||||||
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
public ApiCatalogControllerList(ApiTestFixture factory)
|
||||||
|
|
||||||
public ApiCatalogControllerList(WebTestFixture factory)
|
|
||||||
{
|
{
|
||||||
Client = factory.CreateClient();
|
Client = factory.CreateClient();
|
||||||
}
|
}
|
||||||
@@ -22,7 +20,7 @@ namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnsFirst10CatalogItems()
|
public async Task ReturnsFirst10CatalogItems()
|
||||||
{
|
{
|
||||||
var response = await Client.GetAsync("/api/catalog/list");
|
var response = await Client.GetAsync("/api/catalog-items?pageSize=10");
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||||
var model = stringResponse.FromJson<CatalogIndexViewModel>();
|
var model = stringResponse.FromJson<CatalogIndexViewModel>();
|
||||||
@@ -33,7 +31,7 @@ namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task ReturnsLast2CatalogItemsGivenPageIndex1()
|
public async Task ReturnsLast2CatalogItemsGivenPageIndex1()
|
||||||
{
|
{
|
||||||
var response = await Client.GetAsync("/api/catalog/list?page=1");
|
var response = await Client.GetAsync("/api/catalog-items?pageSize=10&pageIndex=1");
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var stringResponse = await response.Content.ReadAsStringAsync();
|
var stringResponse = await response.Content.ReadAsStringAsync();
|
||||||
var model = stringResponse.FromJson<CatalogIndexViewModel>();
|
var model = stringResponse.FromJson<CatalogIndexViewModel>();
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
|
||||||
|
using Microsoft.eShopWeb.FunctionalTests.Web.Api;
|
||||||
|
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
||||||
|
{
|
||||||
|
[Collection("Sequential")]
|
||||||
|
public class CreateEndpoint : IClassFixture<ApiTestFixture>
|
||||||
|
{
|
||||||
|
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 string _testUri = "test uri";
|
||||||
|
private decimal _testPrice = 1.23m;
|
||||||
|
|
||||||
|
public CreateEndpoint(ApiTestFixture 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<CreateCatalogItemResponse>();
|
||||||
|
|
||||||
|
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(_testUri, model.CatalogItem.PictureUri);
|
||||||
|
Assert.Equal(_testPrice, model.CatalogItem.Price);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StringContent GetValidNewItemJson()
|
||||||
|
{
|
||||||
|
var request = new CreateCatalogItemRequest()
|
||||||
|
{
|
||||||
|
CatalogBrandId = _testBrandId,
|
||||||
|
CatalogTypeId = _testTypeId,
|
||||||
|
Description = _testDescription,
|
||||||
|
Name = _testName,
|
||||||
|
PictureUri = _testUri,
|
||||||
|
Price = _testPrice
|
||||||
|
};
|
||||||
|
var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
return jsonContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
|
||||||
|
using Microsoft.eShopWeb.FunctionalTests.Web.Api;
|
||||||
|
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
||||||
|
{
|
||||||
|
[Collection("Sequential")]
|
||||||
|
public class DeleteEndpoint : IClassFixture<ApiTestFixture>
|
||||||
|
{
|
||||||
|
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
|
public DeleteEndpoint(ApiTestFixture 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<DeleteCatalogItemResponse>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.eShopWeb.FunctionalTests.PublicApi;
|
||||||
|
using Microsoft.eShopWeb.PublicApi.CatalogItemEndpoints;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.eShopWeb.FunctionalTests.Web.Controllers
|
||||||
|
{
|
||||||
|
[Collection("Sequential")]
|
||||||
|
public class GetByIdEndpoint : IClassFixture<ApiTestFixture>
|
||||||
|
{
|
||||||
|
JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
|
public GetByIdEndpoint(ApiTestFixture 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<GetByIdCatalogItemResponse>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user