0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-30 02:31:08 +00:00

Add validation and integration tests

This commit is contained in:
alex289 2023-03-22 17:30:07 +01:00
parent a56fc0e5bb
commit 492ea93b0d
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
9 changed files with 401 additions and 56 deletions

View File

@ -76,7 +76,6 @@ public sealed class UserService : IUserService
public async Task<string> LoginUserAsync(LoginUserViewModel viewModel)
{
return await _bus.QueryAsync(new LoginUserCommand(viewModel.Email, viewModel.Password));
}
}

View File

@ -1,4 +1,8 @@
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
using System.Collections.Generic;
using System.Linq;
using CleanArchitecture.Domain.Commands.Users.ChangePassword;
using CleanArchitecture.Domain.Errors;
using Xunit;
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.ChangePassword;
@ -8,4 +12,83 @@ public sealed class ChangePasswordCommandValidationTests :
public ChangePasswordCommandValidationTests() : base(new ChangePasswordCommandValidation())
{
}
[Fact]
public void Should_Be_Valid()
{
var command = CreateTestCommand();
ShouldBeValid(command);
}
[Fact]
public void Should_Be_Invalid_For_Empty_Password()
{
var command = CreateTestCommand("");
var errors = new List<string>
{
DomainErrorCodes.UserEmptyPassword,
DomainErrorCodes.UserSpecialCharPassword,
DomainErrorCodes.UserNumberPassword,
DomainErrorCodes.UserLowercaseLetterPassword,
DomainErrorCodes.UserUppercaseLetterPassword,
DomainErrorCodes.UserShortPassword
};
ShouldHaveExpectedErrors(command, errors.ToArray());
}
[Fact]
public void Should_Be_Invalid_For_Missing_Special_Character()
{
var command = CreateTestCommand("z8tnayvd5FNLU9AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Number()
{
var command = CreateTestCommand("z]tnayvdFNLU:]AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Lowercase_Character()
{
var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM");
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Uppercase_Character()
{
var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm");
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
}
[Fact]
public void Should_Be_Invalid_For_Password_Too_Short()
{
var command = CreateTestCommand("zA6{");
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
}
[Fact]
public void Should_Be_Invalid_For_Password_Too_Long()
{
var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12));
ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
}
private ChangePasswordCommand CreateTestCommand(
string? password = null, string? newPassword = null) => new(
password ?? "z8]tnayvd5FNLU9:]AQm",
newPassword ?? "z8]tnayvd5FNLU9:]AQw");
}

View File

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
using CleanArchitecture.Domain.Errors;
using Xunit;
@ -108,6 +110,72 @@ public sealed class CreateUserCommandValidationTests :
"Given name may not be longer than 100 characters");
}
[Fact]
public void Should_Be_Invalid_For_Empty_Password()
{
var command = CreateTestCommand(password: "");
var errors = new List<string>
{
DomainErrorCodes.UserEmptyPassword,
DomainErrorCodes.UserSpecialCharPassword,
DomainErrorCodes.UserNumberPassword,
DomainErrorCodes.UserLowercaseLetterPassword,
DomainErrorCodes.UserUppercaseLetterPassword,
DomainErrorCodes.UserShortPassword
};
ShouldHaveExpectedErrors(command, errors.ToArray());
}
[Fact]
public void Should_Be_Invalid_For_Missing_Special_Character()
{
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Number()
{
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Lowercase_Character()
{
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Uppercase_Character()
{
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
}
[Fact]
public void Should_Be_Invalid_For_Password_Too_Short()
{
var command = CreateTestCommand(password: "zA6{");
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
}
[Fact]
public void Should_Be_Invalid_For_Password_Too_Long()
{
var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
}
private CreateUserCommand CreateTestCommand(
Guid? userId = null,
string? email = null,

View File

@ -1,4 +1,8 @@
using CleanArchitecture.Domain.Commands.Users.LoginUser;
using System.Collections.Generic;
using System.Linq;
using CleanArchitecture.Domain.Commands.Users.LoginUser;
using CleanArchitecture.Domain.Errors;
using Xunit;
namespace CleanArchitecture.Domain.Tests.CommandHandler.User.LoginUser;
@ -8,4 +12,118 @@ public sealed class LoginUserCommandValidationTests :
public LoginUserCommandValidationTests() : base(new LoginUserCommandValidation())
{
}
[Fact]
public void Should_Be_Valid()
{
var command = CreateTestCommand();
ShouldBeValid(command);
}
[Fact]
public void Should_Be_Invalid_For_Empty_Email()
{
var command = CreateTestCommand(email: string.Empty);
ShouldHaveSingleError(
command,
DomainErrorCodes.UserInvalidEmail,
"Email is not a valid email address");
}
[Fact]
public void Should_Be_Invalid_For_Invalid_Email()
{
var command = CreateTestCommand(email: "not a email");
ShouldHaveSingleError(
command,
DomainErrorCodes.UserInvalidEmail,
"Email is not a valid email address");
}
[Fact]
public void Should_Be_Invalid_For_Email_Exceeds_Max_Length()
{
var command = CreateTestCommand(email: new string('a', 320) + "@test.com");
ShouldHaveSingleError(
command,
DomainErrorCodes.UserEmailExceedsMaxLength,
"Email may not be longer than 320 characters");
}
[Fact]
public void Should_Be_Invalid_For_Empty_Password()
{
var command = CreateTestCommand(password: "");
var errors = new List<string>
{
DomainErrorCodes.UserEmptyPassword,
DomainErrorCodes.UserSpecialCharPassword,
DomainErrorCodes.UserNumberPassword,
DomainErrorCodes.UserLowercaseLetterPassword,
DomainErrorCodes.UserUppercaseLetterPassword,
DomainErrorCodes.UserShortPassword
};
ShouldHaveExpectedErrors(command, errors.ToArray());
}
[Fact]
public void Should_Be_Invalid_For_Missing_Special_Character()
{
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Number()
{
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Lowercase_Character()
{
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword);
}
[Fact]
public void Should_Be_Invalid_For_Missing_Uppercase_Character()
{
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword);
}
[Fact]
public void Should_Be_Invalid_For_Password_Too_Short()
{
var command = CreateTestCommand(password: "zA6{");
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword);
}
[Fact]
public void Should_Be_Invalid_For_Password_Too_Long()
{
var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword);
}
private LoginUserCommand CreateTestCommand(
string? email = null,
string? password = null) =>
new (
email ?? "test@email.com",
password ?? "Po=PF]PC6t.?8?ks)A6W");
}

View File

@ -70,4 +70,22 @@ public class ValidationTestBase<TCommand, TValidation>
.Be(1);
}
}
protected void ShouldHaveExpectedErrors(
TCommand command,
params string[] expectedErrors)
{
var result = _validation.Validate(command);
result.IsValid.Should().BeFalse();
result.Errors.Count.Should().Be(expectedErrors.Length);
foreach (var error in expectedErrors)
{
result.Errors
.Count(validation => validation.ErrorCode == error)
.Should()
.Be(1);
}
}
}

View File

@ -58,7 +58,7 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase,
return;
}
string passwordHash = BC.HashPassword(request.NewPassword);
var passwordHash = BC.HashPassword(request.NewPassword);
user.SetPassword(passwordHash);
_userRepository.Update(user);

View File

@ -20,7 +20,7 @@ namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
public sealed class LoginUserCommandHandler : CommandHandlerBase,
IRequestHandler<LoginUserCommand, string>
{
private const double EXPIRY_DURATION_MINUTES = 30;
private const double ExpiryDurationMinutes = 30;
private readonly IUserRepository _userRepository;
private readonly TokenSettings _tokenSettings;
@ -76,13 +76,13 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase,
_tokenSettings);
}
public static string BuildToken(string email, UserRole role, Guid Id, TokenSettings tokenSettings)
public static string BuildToken(string email, UserRole role, Guid id, TokenSettings tokenSettings)
{
var claims = new[]
{
new Claim(ClaimTypes.Email, email),
new Claim(ClaimTypes.Role, role.ToString()),
new Claim(ClaimTypes.NameIdentifier, Id.ToString())
new Claim(ClaimTypes.NameIdentifier, id.ToString())
};
var securityKey = new SymmetricSecurityKey(
@ -96,7 +96,7 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase,
tokenSettings.Issuer,
tokenSettings.Audience,
claims,
expires: DateTime.Now.AddMinutes(EXPIRY_DURATION_MINUTES),
expires: DateTime.Now.AddMinutes(ExpiryDurationMinutes),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);

View File

@ -25,31 +25,15 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
}
[Fact, Priority(0)]
public async Task Should_Get_No_User()
{
var response = await _fixture.ServerClient.GetAsync("user");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<IEnumerable<UserViewModel>>();
message?.Data.Should().NotBeNull();
var content = message!.Data!;
content.Should().BeNullOrEmpty();
}
[Fact, Priority(5)]
public async Task Should_Create_User()
{
var user = new CreateUserViewModel(
"test@email.com",
_fixture.CreatedUserEmail,
"Test",
"Email",
"Password");
_fixture.CreatedUserPassword);
var response = await _fixture.ServerClient.PostAsJsonAsync("user", user);
var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user", user);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@ -59,11 +43,49 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
_fixture.CreatedUserId = message!.Data;
}
[Fact, Priority(5)]
public async Task Should_Login_User()
{
var user = new LoginUserViewModel(
_fixture.CreatedUserEmail,
_fixture.CreatedUserPassword);
var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", user);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<string>();
message?.Data.Should().NotBeEmpty();
_fixture.CreatedUserToken = message!.Data!;
_fixture.EnableAuthentication();
}
[Fact, Priority(10)]
public async Task Should_Get_Created_Users()
{
var response = await _fixture.ServerClient.GetAsync("user/" + _fixture.CreatedUserId);
var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<UserViewModel>();
message?.Data.Should().NotBeNull();
var content = message!.Data!;
content.Id.Should().Be(_fixture.CreatedUserId);
content.Email.Should().Be("test@email.com");
content.Surname.Should().Be("Test");
content.GivenName.Should().Be("Email");
}
[Fact, Priority(10)]
public async Task Should_Get_The_Current_Active_Users()
{
var response = await _fixture.ServerClient.GetAsync("/api/v1/user/me");
response.StatusCode.Should().Be(HttpStatusCode.OK);
@ -89,7 +111,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
"NewEmail",
UserRole.User);
var response = await _fixture.ServerClient.PutAsJsonAsync("user", user);
var response = await _fixture.ServerClient.PutAsJsonAsync("/api/v1/user", user);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@ -105,7 +127,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
[Fact, Priority(20)]
public async Task Should_Get_Updated_Users()
{
var response = await _fixture.ServerClient.GetAsync("user/" + _fixture.CreatedUserId);
var response = await _fixture.ServerClient.GetAsync("/api/v1/user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@ -119,12 +141,47 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
content.Email.Should().Be("newtest@email.com");
content.Surname.Should().Be("NewTest");
content.GivenName.Should().Be("NewEmail");
_fixture.CreatedUserEmail = content.Email;
}
[Fact, Priority(25)]
public async Task Should_Change_User_Password()
{
var user = new ChangePasswordViewModel(
_fixture.CreatedUserPassword,
_fixture.CreatedUserPassword + "1");
var response = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/changePassword", user);
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<ChangePasswordViewModel>();
message?.Data.Should().NotBeNull();
var content = message!.Data;
content.Should().BeEquivalentTo(user);
// Verify the user can login with the new password
var login = new LoginUserViewModel(
_fixture.CreatedUserEmail,
_fixture.CreatedUserPassword + "1");
var loginResponse = await _fixture.ServerClient.PostAsJsonAsync("/api/v1/user/login", login);
loginResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var loginMessage = await loginResponse.Content.ReadAsJsonAsync<string>();
loginMessage?.Data.Should().NotBeEmpty();
}
[Fact, Priority(25)]
public async Task Should_Get_One_User()
[Fact, Priority(30)]
public async Task Should_Get_All_User()
{
var response = await _fixture.ServerClient.GetAsync("user");
var response = await _fixture.ServerClient.GetAsync("/api/v1/user");
response.StatusCode.Should().Be(HttpStatusCode.OK);
@ -134,17 +191,27 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
var content = message!.Data!.ToList();
content.Should().ContainSingle();
content.First().Id.Should().Be(_fixture.CreatedUserId);
content.First().Email.Should().Be("newtest@email.com");
content.First().Surname.Should().Be("NewTest");
content.First().GivenName.Should().Be("NewEmail");
content.Count.Should().Be(2);
var currentUser = content.First(x => x.Id == _fixture.CreatedUserId);
currentUser.Id.Should().Be(_fixture.CreatedUserId);
currentUser.Role.Should().Be(UserRole.User);
currentUser.Email.Should().Be("newtest@email.com");
currentUser.Surname.Should().Be("NewTest");
currentUser.GivenName.Should().Be("NewEmail");
var adminUser = content.First(x => x.Role == UserRole.Admin);
adminUser.Email.Should().Be("admin@email.com");
adminUser.Surname.Should().Be("Admin");
adminUser.GivenName.Should().Be("User");
}
[Fact, Priority(30)]
[Fact, Priority(35)]
public async Task Should_Delete_User()
{
var response = await _fixture.ServerClient.DeleteAsync("user/" + _fixture.CreatedUserId);
var response = await _fixture.ServerClient.DeleteAsync("/api/v1/user/" + _fixture.CreatedUserId);
response.StatusCode.Should().Be(HttpStatusCode.OK);
@ -155,20 +222,4 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
var content = message!.Data;
content.Should().Be(_fixture.CreatedUserId);
}
[Fact, Priority(35)]
public async Task Should_Get_No_User_Again()
{
var response = await _fixture.ServerClient.GetAsync("user");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var message = await response.Content.ReadAsJsonAsync<IEnumerable<UserViewModel>>();
message?.Data.Should().NotBeNull();
var content = message!.Data!;
content.Should().BeNullOrEmpty();
}
}

View File

@ -5,4 +5,12 @@ namespace CleanArchitecture.IntegrationTests.Fixtures;
public sealed class UserTestFixture : TestFixtureBase
{
public Guid CreatedUserId { get; set; }
public string CreatedUserEmail { get; set; } = "test@email.com";
public string CreatedUserPassword { get; set; } = "z8]tnayvd5FNLU9:]AQm";
public string CreatedUserToken { get; set; } = string.Empty;
public void EnableAuthentication()
{
ServerClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {CreatedUserToken}");
}
}