From 973b23881cd008c2f5327fc0b6bb488429c0e15f Mon Sep 17 00:00:00 2001 From: Felipe Mattioli Date: Wed, 10 Jul 2024 19:56:49 -0300 Subject: [PATCH] FEAT: Changing logic to the use case --- .../Feijuca.Keycloak.TokenManager.sln | 9 +- .../Users/CreateUserCommandHandler.cs | 25 ++++- .../Mappers/UserMapper.cs | 7 +- .../Requests/User/AttributesRequest.cs | 2 +- .../Entities/Attributes.cs | 9 +- .../Interfaces/IUserRepository.cs | 3 +- .../Extensions/RepositoriesExtension.cs | 3 - .../Repositories/UserRepository.cs | 50 ++++------ .../Handlers/CreateUserCommandHandlerTests.cs | 95 +++++++++++++++++++ .../Handlers/LoginUserCommandHandlerTests.cs | 95 +++++++++++++++++++ .../TokenManager.UnitTests.csproj | 12 ++- tests/TokenManager.UnitTests/UnitTest1.cs | 11 --- 12 files changed, 246 insertions(+), 75 deletions(-) create mode 100644 tests/TokenManager.UnitTests/Handlers/CreateUserCommandHandlerTests.cs create mode 100644 tests/TokenManager.UnitTests/Handlers/LoginUserCommandHandlerTests.cs delete mode 100644 tests/TokenManager.UnitTests/UnitTest1.cs diff --git a/src/Feijuca.Keycloak.TokenManager/Feijuca.Keycloak.TokenManager.sln b/src/Feijuca.Keycloak.TokenManager/Feijuca.Keycloak.TokenManager.sln index 2903fea..e0dd742 100644 --- a/src/Feijuca.Keycloak.TokenManager/Feijuca.Keycloak.TokenManager.sln +++ b/src/Feijuca.Keycloak.TokenManager/Feijuca.Keycloak.TokenManager.sln @@ -19,12 +19,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenManager.Infra.CrossCut EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenManager.Domain", "TokenManager.Domain\TokenManager.Domain.csproj", "{EC94BA98-EAF7-4118-9619-08D077929FDE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TokenManager.UnitTests", "..\..\tests\TokenManager.UnitTests\TokenManager.UnitTests.csproj", "{058EA4DF-2449-404C-B219-FBA6754FD5B9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenManager.UnitTests", "..\..\tests\TokenManager.UnitTests\TokenManager.UnitTests.csproj", "{058EA4DF-2449-404C-B219-FBA6754FD5B9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{00FBC958-54AF-4A97-893C-15667D843B3E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TokenManager.IntegrationTests", "..\..\tests\TokenManager.IntegrationTests\TokenManager.IntegrationTests.csproj", "{B747623C-977F-4377-BF24-6F7A00826306}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,10 +53,6 @@ Global {058EA4DF-2449-404C-B219-FBA6754FD5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {058EA4DF-2449-404C-B219-FBA6754FD5B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {058EA4DF-2449-404C-B219-FBA6754FD5B9}.Release|Any CPU.Build.0 = Release|Any CPU - {B747623C-977F-4377-BF24-6F7A00826306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B747623C-977F-4377-BF24-6F7A00826306}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B747623C-977F-4377-BF24-6F7A00826306}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B747623C-977F-4377-BF24-6F7A00826306}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,7 +64,6 @@ Global {3C751694-F055-43CD-B374-62C352D385B3} = {59BDDB9A-A2B6-4366-9522-803AC05E850B} {EC94BA98-EAF7-4118-9619-08D077929FDE} = {DCC0F26F-67F8-4431-8564-5B9F6E1D3F47} {058EA4DF-2449-404C-B219-FBA6754FD5B9} = {00FBC958-54AF-4A97-893C-15667D843B3E} - {B747623C-977F-4377-BF24-6F7A00826306} = {00FBC958-54AF-4A97-893C-15667D843B3E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C84D43BB-2657-4F8D-AC09-2736B1514A82} diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommandHandler.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommandHandler.cs index eb7edf5..0f6d0fc 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommandHandler.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Commands/Users/CreateUserCommandHandler.cs @@ -1,6 +1,8 @@ using Contracts.Common; using MediatR; using TokenManager.Application.Services.Mappers; +using TokenManager.Domain.Entities; +using TokenManager.Domain.Errors; using TokenManager.Domain.Interfaces; namespace TokenManager.Application.Services.Commands.Users @@ -11,13 +13,28 @@ public class CreateUserCommandHandler(IUserRepository userRepository) : IRequest public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) { - var resultUserCreated = await _userRepository.CreateNewUserActions(request.Tenant, request.AddUserRequest.ToDomain()); - if (resultUserCreated.IsSuccess) + var accessTokenResult = await _userRepository.GetAccessTokenAsync(request.Tenant); + if (accessTokenResult.IsSuccess) { - return Result.Success(); + var user = request.AddUserRequest.ToDomain(); + var(IsSuccessStatusCode, contentRequest) = await _userRepository.CreateNewUserAsync(user); + if (IsSuccessStatusCode) + { + await SetUserPasswordAsync(user); + return Result.Success(); + } + + UserErrors.SetTechnicalMessage(contentRequest); + return Result.Failure(UserErrors.TokenGenerationError); } - return Result.Failure(resultUserCreated.Error); + return Result.Failure(UserErrors.InvalidUserNameOrPasswordError); + } + + private async Task SetUserPasswordAsync(User user) + { + var keycloakUser = await _userRepository.GetUserAsync(user.Username!); + await _userRepository.ResetPasswordAsync(keycloakUser.Value.Id!, user.Password!); } } } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Mappers/UserMapper.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Mappers/UserMapper.cs index 7c0b747..8bfea2b 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Mappers/UserMapper.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Mappers/UserMapper.cs @@ -14,10 +14,10 @@ public static User ToDomain(this AddUserRequest userRequest) public static Attributes ToDomain(this AttributesRequest attributes) { - return new Attributes(attributes.ZoneInfo, attributes.Birthdate, attributes.PhoneNumber, attributes.Gender, attributes.Fullname, attributes.Tenant, attributes.Picture); + return new Attributes(attributes.Tenant, attributes.UserAttributes); } - public static User ToDomain(this LoginUserRequest loginUserRequest, string tenant) + public static User ToDomain(this LoginUserRequest loginUserRequest) { return new User(loginUserRequest.Username, loginUserRequest.Password); } @@ -29,7 +29,8 @@ public static TokenResponse ToTokenResponse(this TokenDetails tokenDetails) AccessToken = tokenDetails.Access_Token, ExpiresIn = tokenDetails.Expires_In, RefreshToken = tokenDetails.Refresh_Token, - RefreshExpiresIn = tokenDetails.Refresh_Expires_In + RefreshExpiresIn = tokenDetails.Refresh_Expires_In, + TokenType = tokenDetails.Token_Type, }; } } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AttributesRequest.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AttributesRequest.cs index 43943bd..da339d0 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AttributesRequest.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Application.Services/Requests/User/AttributesRequest.cs @@ -1,4 +1,4 @@ namespace TokenManager.Application.Services.Requests.User { - public record AttributesRequest(string? Tenant, string? ZoneInfo, string? Birthdate, string? PhoneNumber, string? Gender, string? Fullname, string? Picture); + public record AttributesRequest(string Tenant, Dictionary UserAttributes); } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Entities/Attributes.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Entities/Attributes.cs index 988644d..e413ae3 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Entities/Attributes.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Entities/Attributes.cs @@ -1,13 +1,8 @@ namespace TokenManager.Domain.Entities { - public class Attributes(string? Zoneinfo, string? Birthdate, string? PhoneNumber, string? Gender, string? Fullname, string? Tenant, string? Picture) + public class Attributes(string Tenant, Dictionary UserAttributes) { - public string? Zoneinfo { get; set; } = Zoneinfo; - public string? Birthdate { get; set; } = Birthdate; - public string? PhoneNumber { get; set; } = PhoneNumber; - public string? Gender { get; set; } = Gender; - public string? Fullname { get; set; } = Fullname; + public Dictionary UserAttributes = UserAttributes; public string? Tenant { get; set; } = Tenant; - public string? Picture { get; set; } = Picture; } } diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Interfaces/IUserRepository.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Interfaces/IUserRepository.cs index f40db67..3d4ed54 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Interfaces/IUserRepository.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Domain/Interfaces/IUserRepository.cs @@ -6,9 +6,8 @@ namespace TokenManager.Domain.Interfaces public interface IUserRepository { Task> GetAccessTokenAsync(string tenant); - Task CreateNewUserActions(string tenant, User user); Task> LoginAsync(string tenant, User user); - Task CreateNewUserAsync(User user); + Task<(bool result, string content)> CreateNewUserAsync(User user); Task> GetUserAsync(string userName); Task ResetPasswordAsync(string userId, string password); Task SendEmailVerificationAsync(string userId); diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.CrossCutting/Extensions/RepositoriesExtension.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.CrossCutting/Extensions/RepositoriesExtension.cs index 99f8821..4c0c32e 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.CrossCutting/Extensions/RepositoriesExtension.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.CrossCutting/Extensions/RepositoriesExtension.cs @@ -1,9 +1,6 @@ using Feijuca.Keycloak.MultiTenancy.Services; using Feijuca.Keycloak.MultiTenancy.Services.Models; using Microsoft.Extensions.DependencyInjection; - -using System; - using TokenManager.Domain.Interfaces; using TokenManager.Infra.Data.Models; using TokenManager.Infra.Data.Repositories; diff --git a/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.Data/Repositories/UserRepository.cs b/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.Data/Repositories/UserRepository.cs index 1ebb4c5..bb9b4eb 100644 --- a/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.Data/Repositories/UserRepository.cs +++ b/src/Feijuca.Keycloak.TokenManager/TokenManager.Infra.Data/Repositories/UserRepository.cs @@ -14,7 +14,6 @@ namespace TokenManager.Infra.Data.Repositories public class UserRepository : IUserRepository { private readonly IHttpClientFactory _httpClientFactory; - private readonly IAuthService _authService; private readonly TokenCredentials _tokenCredentials; private readonly HttpClient _httpClient; private string _urlUserActions = ""; @@ -24,10 +23,9 @@ public class UserRepository : IUserRepository NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore }; - public UserRepository(IHttpClientFactory httpClientFactory, IAuthService authService, TokenCredentials tokenCredentials) + public UserRepository(IHttpClientFactory httpClientFactory, TokenCredentials tokenCredentials) { _httpClientFactory = httpClientFactory; - _authService = authService; _tokenCredentials = tokenCredentials; _httpClient = _httpClientFactory.CreateClient("KeycloakClient"); } @@ -51,9 +49,12 @@ public async Task> GetAccessTokenAsync(string tenant) var response = await _httpClient.PostAsync(url, requestData); if (response.IsSuccessStatusCode) - { + { var content = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(content); + var result = JsonConvert.DeserializeObject(content)!; + + SetAuthorizationHeader(result.Access_Token); + return Result.Success(result!); } @@ -61,37 +62,15 @@ public async Task> GetAccessTokenAsync(string tenant) UserErrors.SetTechnicalMessage(responseMessage); return Result.Failure(UserErrors.TokenGenerationError); } - - public async Task CreateNewUserActions(string tenant, User user) + + public async Task<(bool result, string content)> CreateNewUserAsync(User user) { - var tokenBearerResult = await GetAccessTokenAsync(tenant); - if (tokenBearerResult.IsSuccess) - { - ConfigureHttpClient(tenant, tokenBearerResult.Value.Access_Token); - - var response = await CreateNewUserAsync(user); - - if (response.IsSuccessStatusCode) - { - var keycloakUser = await GetUserAsync(user.Username!); - await ResetPasswordAsync(keycloakUser.Value.Id!, user.Password!); - return Result.Success(); - } - - var responseMessage = await response.Content.ReadAsStringAsync(); - UserErrors.SetTechnicalMessage(responseMessage); - return Result.Failure(UserErrors.TokenGenerationError); - } + SetBaseUrlUserAction(user!.Attributes!.Tenant!); - return Result.Failure(UserErrors.InvalidUserNameOrPasswordError); - } - - public async Task CreateNewUserAsync(User user) - { var json = JsonConvert.SerializeObject(user, Settings); var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(_urlUserActions, httpContent); - return response; + return (response.IsSuccessStatusCode, await response.Content.ReadAsStringAsync()); } public async Task> GetUserAsync(string userName) @@ -189,15 +168,18 @@ public async Task> LoginAsync(string tenant, User user) return Result.Failure(UserErrors.InvalidUserNameOrPasswordError); } - private void ConfigureHttpClient(string tenant, string accessToken) + private void SetBaseUrlUserAction(string tenant) { - _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); - _urlUserActions = _httpClient.BaseAddress .AppendPathSegment("admin") .AppendPathSegment("realms") .AppendPathSegment(tenant) .AppendPathSegment("users"); } + + private void SetAuthorizationHeader(string accessToken) + { + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); + } } } diff --git a/tests/TokenManager.UnitTests/Handlers/CreateUserCommandHandlerTests.cs b/tests/TokenManager.UnitTests/Handlers/CreateUserCommandHandlerTests.cs new file mode 100644 index 0000000..aba8c8a --- /dev/null +++ b/tests/TokenManager.UnitTests/Handlers/CreateUserCommandHandlerTests.cs @@ -0,0 +1,95 @@ +using AutoFixture; +using Contracts.Common; +using FluentAssertions; +using Moq; +using TokenManager.Application.Services.Commands.Users; +using TokenManager.Domain.Entities; +using TokenManager.Domain.Errors; +using TokenManager.Domain.Interfaces; + +namespace TokenManager.UnitTests.Handlers +{ + public class CreateUserCommandHandlerTests + { + private readonly Fixture _autoFixture = new(); + private readonly Mock _userRepositoryMock = new(); + private readonly CreateUserCommandHandler _createUserCommandHandler; + + public CreateUserCommandHandlerTests() + { + _createUserCommandHandler = new(_userRepositoryMock.Object); + } + + [Fact] + public async Task HandleWhenInformAValidUser_ShouldCreateANewUserAndReturnsTrue() + { + // Arrange + var createUserCommand = _autoFixture.Create(); + var tokenDetails = _autoFixture.Create(); + var successResult = Result.Success(tokenDetails); + + _userRepositoryMock + .Setup(x => x.GetAccessTokenAsync(It.IsAny())) + .ReturnsAsync(successResult); + + (bool result, string content) createUserResult = (true, "Success message"); + + _userRepositoryMock + .Setup(x => x.CreateNewUserAsync(It.IsAny())) + .ReturnsAsync(createUserResult); + + var user = _autoFixture.Create(); + var userResult = Result.Success(user); + + _userRepositoryMock + .Setup(x => x.GetUserAsync(It.IsAny())) + .ReturnsAsync(userResult); + + _userRepositoryMock + .Setup(x => x.ResetPasswordAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(userResult); + + //Act + var resultHandle = await _createUserCommandHandler.Handle(createUserCommand, CancellationToken.None); + + //Assert + _userRepositoryMock + .Verify(x => x.GetAccessTokenAsync(It.IsAny()), Times.Once); + + resultHandle.IsSuccess + .Should() + .Be(true); + } + + [Fact] + public async Task HandleWhenInformAInvalidUser_ShouldNotCreateANewUserAndReturnsFalse() + { + // Arrange + var createUserCommand = _autoFixture.Create(); + + var errorMessage = _autoFixture.Create(); + UserErrors.SetTechnicalMessage(errorMessage); + var result = Result.Failure(UserErrors.TokenGenerationError); + + _userRepositoryMock + .Setup(x => x.GetAccessTokenAsync(It.IsAny()), Times.Once) + .ReturnsAsync(result); + + //Act + var resultHandle = await _createUserCommandHandler.Handle(createUserCommand, CancellationToken.None); + + //Assert + resultHandle.IsSuccess + .Should() + .Be(false); + + resultHandle.Error.Description + .Should() + .Contain(result.Error.Description); + + resultHandle.Error.Code + .Should() + .Contain(result.Error.Code); + } + } +} diff --git a/tests/TokenManager.UnitTests/Handlers/LoginUserCommandHandlerTests.cs b/tests/TokenManager.UnitTests/Handlers/LoginUserCommandHandlerTests.cs new file mode 100644 index 0000000..850a611 --- /dev/null +++ b/tests/TokenManager.UnitTests/Handlers/LoginUserCommandHandlerTests.cs @@ -0,0 +1,95 @@ +using AutoFixture; + +using Contracts.Common; +using FluentAssertions; +using Moq; +using TokenManager.Application.Services.Commands.Users; +using TokenManager.Domain.Entities; +using TokenManager.Domain.Interfaces; + +namespace TokenManager.UnitTests.Handlers +{ + public class LoginUserCommandHandlerTests + { + private readonly Fixture _autoFixture = new(); + private readonly Mock _userRepositoryMock = new(); + private readonly LoginUserCommandHandler _loginUserCommandHandler; + + public LoginUserCommandHandlerTests() + { + _loginUserCommandHandler = new(_userRepositoryMock.Object); + } + + [Fact] + public async Task HandleWhenInformAValidUser_ShouldLoggedAndGenerateNewTokenDetails() + { + // Arrange + var loginUserCommand = _autoFixture.Create(); + + var tokenDetails = _autoFixture.Create(); + var successResult = Result.Success(tokenDetails); + + _userRepositoryMock + .Setup(x => x.LoginAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(successResult); + + //Act + var resultHandle = await _loginUserCommandHandler.Handle(loginUserCommand, CancellationToken.None); + + //Assert + resultHandle.IsSuccess + .Should() + .Be(true); + + resultHandle.Value.AccessToken + .Should() + .Be(tokenDetails.Access_Token); + + resultHandle.Value.ExpiresIn + .Should() + .Be(tokenDetails.Expires_In); + + resultHandle.Value.RefreshExpiresIn + .Should() + .Be(tokenDetails.Refresh_Expires_In); + + resultHandle.Value.RefreshToken + .Should() + .Be(tokenDetails.Refresh_Token); + + resultHandle.Value.TokenType + .Should() + .Be(tokenDetails.Token_Type); + } + + [Fact] + public async Task HandleWhenInformAInvalidUser_ShouldNotBeLoggedAndShouldReturnsAnError() + { + // Arrange + var loginUserCommand = _autoFixture.Create(); + + var error = _autoFixture.Create(); + var failureResult = Result.Failure(error); + + _userRepositoryMock + .Setup(x => x.LoginAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(failureResult); + + //Act + var resultHandle = await _loginUserCommandHandler.Handle(loginUserCommand, CancellationToken.None); + + //Assert + resultHandle.IsSuccess + .Should() + .Be(false); + + resultHandle.Error.Description + .Should() + .Contain(error.Description); + + resultHandle.Error.Code + .Should() + .Contain(error.Code); + } + } +} diff --git a/tests/TokenManager.UnitTests/TokenManager.UnitTests.csproj b/tests/TokenManager.UnitTests/TokenManager.UnitTests.csproj index a83e0ca..c9fc500 100644 --- a/tests/TokenManager.UnitTests/TokenManager.UnitTests.csproj +++ b/tests/TokenManager.UnitTests/TokenManager.UnitTests.csproj @@ -10,18 +10,26 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/tests/TokenManager.UnitTests/UnitTest1.cs b/tests/TokenManager.UnitTests/UnitTest1.cs deleted file mode 100644 index b5208a2..0000000 --- a/tests/TokenManager.UnitTests/UnitTest1.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace TokenManager.UnitTests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} \ No newline at end of file