0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-08-24 12:18:34 +00:00

Add Swagger and domain tests

This commit is contained in:
Alexander Konietzko 2023-03-21 11:03:05 +01:00
parent 3b1a76438b
commit a56fc0e5bb
No known key found for this signature in database
GPG Key ID: BA6905F37AEC2B5B
18 changed files with 406 additions and 14 deletions

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
@ -15,6 +15,7 @@
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,16 +1,19 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CleanArchitecture.Api.Models;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Notifications;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace CleanArchitecture.Api.Controllers;
[ApiController]
[Route("[controller]")]
[Route("/api/v1/[controller]")]
public class UserController : ApiController
{
private readonly IUserService _userService;
@ -24,6 +27,8 @@ public class UserController : ApiController
[Authorize]
[HttpGet]
[SwaggerOperation("Get a list of all users")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<UserViewModel>>))]
public async Task<IActionResult> GetAllUsersAsync()
{
var users = await _userService.GetAllUsersAsync();
@ -32,6 +37,8 @@ public class UserController : ApiController
[Authorize]
[HttpGet("{id}")]
[SwaggerOperation("Get a user by id")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
public async Task<IActionResult> GetUserByIdAsync(
[FromRoute] Guid id,
[FromQuery] bool isDeleted = false)
@ -42,6 +49,8 @@ public class UserController : ApiController
[Authorize]
[HttpGet("me")]
[SwaggerOperation("Get the current active user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
public async Task<IActionResult> GetCurrentUserAsync()
{
var user = await _userService.GetCurrentUserAsync();
@ -49,6 +58,8 @@ public class UserController : ApiController
}
[HttpPost]
[SwaggerOperation("Create a new user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserViewModel viewModel)
{
var userId = await _userService.CreateUserAsync(viewModel);
@ -57,6 +68,8 @@ public class UserController : ApiController
[Authorize]
[HttpDelete("{id}")]
[SwaggerOperation("Delete a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
public async Task<IActionResult> DeleteUserAsync([FromRoute] Guid id)
{
await _userService.DeleteUserAsync(id);
@ -65,6 +78,8 @@ public class UserController : ApiController
[Authorize]
[HttpPut]
[SwaggerOperation("Update a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UpdateUserViewModel>))]
public async Task<IActionResult> UpdateUserAsync([FromBody] UpdateUserViewModel viewModel)
{
await _userService.UpdateUserAsync(viewModel);
@ -73,6 +88,8 @@ public class UserController : ApiController
[Authorize]
[HttpPost("changePassword")]
[SwaggerOperation("Change a password for the current active user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<ChangePasswordViewModel>))]
public async Task<IActionResult> ChangePasswordAsync([FromBody] ChangePasswordViewModel viewModel)
{
await _userService.ChangePasswordAsync(viewModel);
@ -80,6 +97,8 @@ public class UserController : ApiController
}
[HttpPost("login")]
[SwaggerOperation("Get a signed token for a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<string>))]
public async Task<IActionResult> LoginUserAsync([FromBody] LoginUserViewModel viewModel)
{
var token = await _userService.LoginUserAsync(viewModel);

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text;
using CleanArchitecture.Application.Extensions;
using CleanArchitecture.Domain.Extensions;
@ -10,15 +11,57 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddGrpc();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(c =>
{
c.EnableAnnotations();
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "CleanArchitecture",
Version = "v1",
Description = "A clean architecture API",
});
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. " +
"Use the /auth/azureLogin endpoint to generate a token (use the id_token here), " +
"or create a personal access token in centralhub.",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
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>()
}
});
});
builder.Services.AddHealthChecks();
builder.Services.AddHttpContextAccessor();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
@ -58,8 +101,11 @@ builder.Services.AddMediatR(cfg =>
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
@ -67,6 +113,7 @@ app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.MapGrpcService<UsersApiImplementation>();
using (IServiceScope scope = app.Services.CreateScope())

View File

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.18.4" />

View File

@ -0,0 +1,63 @@
using System.Threading.Tasks;
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User;
using Xunit;
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword;
public sealed class ChangePasswordCommandHandlerTests
{
private readonly ChangePasswordCommandTestFixture _fixture = new();
[Fact]
public async Task Should_Change_Password()
{
var user = _fixture.SetupUser();
var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQm", "z8]tnayvd5FNLU9:]AQw");
await _fixture.CommandHandler.Handle(command, default);
_fixture
.VerifyNoDomainNotification()
.VerifyCommit()
.VerifyRaisedEvent<PasswordChangedEvent>(x => x.UserId == user.Id);
}
[Fact]
public async Task Should_Not_Change_Password_No_User()
{
var userId = _fixture.SetupMissingUser();
var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQm", "z8]tnayvd5FNLU9:]AQw");
await _fixture.CommandHandler.Handle(command, default);
_fixture
.VerifyNoCommit()
.VerifyNoRaisedEvent<UserUpdatedEvent>()
.VerifyAnyDomainNotification()
.VerifyExistingNotification(
ErrorCodes.ObjectNotFound,
$"There is no User with Id {userId}");
}
[Fact]
public async Task Should_Not_Change_Password_Incorrect_Password()
{
_fixture.SetupUser();
var command = new ChangePasswordCommand("z8]tnayvd5FNLU9:]AQw", "z8]tnayvd5FNLU9:]AQx");
await _fixture.CommandHandler.Handle(command, default);
_fixture
.VerifyNoCommit()
.VerifyNoRaisedEvent<UserUpdatedEvent>()
.VerifyAnyDomainNotification()
.VerifyExistingNotification(
DomainErrorCodes.UserPasswordIncorrect,
"The password is incorrect");
}
}

View File

@ -0,0 +1,52 @@
using System;
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces.Repositories;
using Moq;
using BC = BCrypt.Net.BCrypt;
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword;
public sealed class ChangePasswordCommandTestFixture : CommandHandlerFixtureBase
{
public ChangePasswordCommandHandler CommandHandler { get; set; }
public Mock<IUserRepository> UserRepository { get; set; }
public ChangePasswordCommandTestFixture()
{
UserRepository = new Mock<IUserRepository>();
CommandHandler = new(
Bus.Object,
UnitOfWork.Object,
NotificationHandler.Object,
UserRepository.Object,
User.Object);
}
public Entities.User SetupUser()
{
var user = new Entities.User(
Guid.NewGuid(),
"max@mustermann.com",
"Max",
"Mustermann",
BC.HashPassword("z8]tnayvd5FNLU9:]AQm"),
UserRole.User);
User.Setup(x => x.GetUserId()).Returns(user.Id);
UserRepository
.Setup(x => x.GetByIdAsync(It.Is<Guid>(y => y == user.Id)))
.ReturnsAsync(user);
return user;
}
public Guid SetupMissingUser()
{
var id = Guid.NewGuid();
User.Setup(x => x.GetUserId()).Returns(id);
return id;
}
}

View File

@ -0,0 +1,11 @@
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword;
public sealed class ChangePasswordCommandValidationTests :
ValidationTestBase<ChangePasswordCommand, ChangePasswordCommandValidation>
{
public ChangePasswordCommandValidationTests() : base(new ChangePasswordCommandValidation())
{
}
}

View File

@ -20,7 +20,7 @@ public sealed class CreateUserCommandHandlerTests
"test@email.com",
"Test",
"Email",
"SomePassword");
"Po=PF]PC6t.?8?ks)A6W");
_fixture.CommandHandler.Handle(command, default).Wait();
@ -40,7 +40,7 @@ public sealed class CreateUserCommandHandlerTests
"test@email.com",
"Test",
"Email",
"SomePassword");
"Po=PF]PC6t.?8?ks)A6W");
_fixture.CommandHandler.Handle(command, default).Wait();

View File

@ -119,5 +119,5 @@ public sealed class CreateUserCommandValidationTests :
email ?? "test@email.com",
surName ?? "test",
givenName ?? "email",
password ?? "some password");
password ?? "Po=PF]PC6t.?8?ks)A6W");
}

View File

@ -0,0 +1,82 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Commands.Users.LoginUser;
using CleanArchitecture.Domain.Errors;
using FluentAssertions;
using Xunit;
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser;
public sealed class LoginUserCommandHandlerTests
{
private readonly LoginUserCommandTestFixture _fixture = new();
[Fact]
public async Task Should_Login_User()
{
var user = _fixture.SetupUser();
var command = new LoginUserCommand(user.Email, "z8]tnayvd5FNLU9:]AQm");
var token = await _fixture.CommandHandler.Handle(command, default);
_fixture.VerifyNoDomainNotification();
token.Should().NotBeNullOrEmpty();
var handler = new JwtSecurityTokenHandler();
var decodedToken = handler.ReadToken(token) as JwtSecurityToken;
var userIdClaim = decodedToken!.Claims
.FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.NameIdentifier));
Guid.Parse(userIdClaim!.Value).Should().Be(user.Id);
var userEmailClaim = decodedToken!.Claims
.FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Email));
userEmailClaim!.Value.Should().Be(user.Email);
var userRoleClaim = decodedToken!.Claims
.FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Role));
userRoleClaim!.Value.Should().Be(user.Role.ToString());
}
[Fact]
public async Task Should_Not_Login_User_No_User()
{
var command = new LoginUserCommand("test@email.com", "z8]tnayvd5FNLU9:]AQm");
var token = await _fixture.CommandHandler.Handle(command, default);
_fixture
.VerifyAnyDomainNotification()
.VerifyExistingNotification(
ErrorCodes.ObjectNotFound,
$"There is no User with Email {command.Email}");
token.Should().BeEmpty();
}
[Fact]
public async Task Should_Not_Login_User_Incorrect_Password()
{
var user = _fixture.SetupUser();
var command = new LoginUserCommand(user.Email, "z8]tnayvd5FNLU9:]AQw");
var token = await _fixture.CommandHandler.Handle(command, default);
_fixture
.VerifyAnyDomainNotification()
.VerifyExistingNotification(
DomainErrorCodes.UserPasswordIncorrect,
"The password is incorrect");
token.Should().BeEmpty();
}
}

View File

@ -0,0 +1,55 @@
using CleanArchitecture.Domain.Commands.Users.LoginUser;
using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces.Repositories;
using CleanArchitecture.Domain.Settings;
using Microsoft.Extensions.Options;
using Moq;
using System;
using BC = BCrypt.Net.BCrypt;
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser;
public sealed class LoginUserCommandTestFixture : CommandHandlerFixtureBase
{
public LoginUserCommandHandler CommandHandler { get; set; }
public Mock<IUserRepository> UserRepository { get; set; }
public IOptions<TokenSettings> TokenSettings { get; set; }
public LoginUserCommandTestFixture()
{
UserRepository = new Mock<IUserRepository>();
TokenSettings = Options.Create(new TokenSettings
{
Issuer = "TestIssuer",
Audience = "TestAudience",
Secret = "asjdlkasjd87439284)@#(*"
});
CommandHandler = new(
Bus.Object,
UnitOfWork.Object,
NotificationHandler.Object,
UserRepository.Object,
TokenSettings);
}
public Entities.User SetupUser()
{
var user = new Entities.User(
Guid.NewGuid(),
"max@mustermann.com",
"Max",
"Mustermann",
BC.HashPassword("z8]tnayvd5FNLU9:]AQm"),
UserRole.User);
User.Setup(x => x.GetUserId()).Returns(user.Id);
UserRepository
.Setup(x => x.GetByEmailAsync(It.Is<string>(y => y == user.Email)))
.ReturnsAsync(user);
return user;
}
}

View File

@ -0,0 +1,11 @@
using CleanArchitecture.Domain.Commands.Users.LoginUser;
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser;
public sealed class LoginUserCommandValidationTests :
ValidationTestBase<LoginUserCommand, LoginUserCommandValidation>
{
public LoginUserCommandValidationTests() : base(new LoginUserCommandValidation())
{
}
}

View File

@ -109,7 +109,7 @@ public sealed class UpdateUserCommandValidationTests :
"Given name may not be longer than 100 characters");
}
private UpdateUserCommand CreateTestCommand(
private static UpdateUserCommand CreateTestCommand(
Guid? userId = null,
string? email = null,
string? surName = null,

View File

@ -1,7 +1,25 @@
using FluentValidation;
using CleanArchitecture.Domain.Extensions.Validation;
using FluentValidation;
namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
public sealed class ChangePasswordCommandValidation : AbstractValidator<ChangePasswordCommand>
{
public ChangePasswordCommandValidation()
{
AddRuleForPassword();
AddRuleForNewPassword();
}
private void AddRuleForPassword()
{
RuleFor(cmd => cmd.Password)
.Password();
}
private void AddRuleForNewPassword()
{
RuleFor(cmd => cmd.NewPassword)
.Password();
}
}

View File

@ -1,7 +1,31 @@
using FluentValidation;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Extensions.Validation;
using FluentValidation;
namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
public sealed class LoginUserCommandValidation : AbstractValidator<LoginUserCommand>
{
public LoginUserCommandValidation()
{
AddRuleForEmail();
AddRuleForPassword();
}
private void AddRuleForEmail()
{
RuleFor(cmd => cmd.Email)
.EmailAddress()
.WithErrorCode(DomainErrorCodes.UserInvalidEmail)
.WithMessage("Email is not a valid email address")
.MaximumLength(320)
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength)
.WithMessage("Email may not be longer than 320 characters");
}
private void AddRuleForPassword()
{
RuleFor(cmd => cmd.Password)
.Password();
}
}

View File

@ -11,6 +11,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
AddRuleForEmail();
AddRuleForSurname();
AddRuleForGivenName();
AddRuleForRole();
}
private void AddRuleForId()
@ -53,4 +54,12 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
.WithErrorCode(DomainErrorCodes.UserGivenNameExceedsMaxLength)
.WithMessage("Given name may not be longer than 100 characters");
}
private void AddRuleForRole()
{
RuleFor(cmd => cmd.Role)
.IsInEnum()
.WithErrorCode(DomainErrorCodes.UserInvalidRole)
.WithMessage("Role is not a valid role");
}
}

View File

@ -10,7 +10,8 @@ public static class DomainErrorCodes
public const string UserSurnameExceedsMaxLength = "USER_SURNAME_EXCEEDS_MAX_LENGTH";
public const string UserGivenNameExceedsMaxLength = "USER_GIVEN_NAME_EXCEEDS_MAX_LENGTH";
public const string UserInvalidEmail = "USER_INVALID_EMAIL";
public const string UserInvalidRole = "USER_INVALID_ROLE";
// User Password Validation
public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY";
public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS";

View File

@ -1,2 +0,0 @@
- Remove warnings and apply suggestions
- Add authentication and authorization