diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 480bae0..b627344 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -249,7 +249,7 @@ jobs: name: "Create terraform destroy plan" needs: [create-app] runs-on: ubuntu-latest - + environment: dev steps: - name: Create plan uses: hashicorp/tfc-workflows-github/actions/create-run@v1.3.1 diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Application/UseCases/Orders/GetOrderDetailsUseCase.cs b/src/FIAP.TechChallenge.ByteMeBurger.Application/UseCases/Orders/GetOrderDetailsUseCase.cs index 9bf8793..2ac2aa6 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Application/UseCases/Orders/GetOrderDetailsUseCase.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Application/UseCases/Orders/GetOrderDetailsUseCase.cs @@ -3,10 +3,21 @@ namespace FIAP.TechChallenge.ByteMeBurger.Application.UseCases.Orders; -public class GetOrderDetailsUseCase(IOrderRepository repository) : IGetOrderDetailsUseCase +public class GetOrderDetailsUseCase(IOrderRepository repository, ICustomerRepository customerRepository) + : IGetOrderDetailsUseCase { public async Task Execute(Guid id) { - return id == Guid.Empty ? null : await repository.GetAsync(id); + var order = await GetOrder(id); + if (order is null) return null; + + if (order.Customer is not null) + order.SetCustomer(await GetCustomer(order.Customer.Id)!); + + return order; } + + private async Task GetOrder(Guid id) => id == Guid.Empty ? null : await repository.GetAsync(id); + + private Task GetCustomer(Guid id) => customerRepository.FindByIdAsync(id); } diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoUserManager.cs b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoUserManager.cs index cf96717..869a776 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoUserManager.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoUserManager.cs @@ -1,51 +1,49 @@ -using System.Security.Cryptography; -using FIAP.TechChallenge.ByteMeBurger.Domain.Entities; +using FIAP.TechChallenge.ByteMeBurger.Domain.Entities; using FIAP.TechChallenge.ByteMeBurger.Domain.Interfaces; using Amazon.CognitoIdentityProvider; using Amazon.CognitoIdentityProvider.Model; using FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Factory; using FIAP.TechChallenge.ByteMeBurger.Domain.Base; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway; -public class CognitoUserManager : ICustomerRepository +public class CognitoUserManager( + ICognitoClientFactory cognitoClientFactory, + ILogger logger, + IOptions settings) + : ICustomerRepository { - private readonly IAmazonCognitoIdentityProvider _cognitoClient; - private readonly string _userPoolId; - private readonly string _clientId; - - public CognitoUserManager(ICognitoClientFactory cognitoClientFactory, IOptions settings) - { - _cognitoClient = cognitoClientFactory.CreateClient(); - _userPoolId = settings.Value.UserPoolId; - _clientId = settings.Value.UserPoolClientId; - } + private readonly IAmazonCognitoIdentityProvider _cognitoClient = cognitoClientFactory.CreateClient(); + private readonly string _userPoolId = settings.Value.UserPoolId; public async Task FindByCpfAsync(string cpf) { try { + logger.LogInformation("Fetching user with CPF {cpf}", cpf); var response = await _cognitoClient.AdminGetUserAsync(new AdminGetUserRequest { UserPoolId = _userPoolId, - Username = cpf + Username = cpf, }); - var email = response.UserAttributes.First(attr => attr.Name == "email").Value; - var name = response.UserAttributes.First(attr => attr.Name == "name").Value; - var sub = response.UserAttributes.First(attr => attr.Name == "sub").Value; - var customer = new Customer(Guid.Parse(sub), cpf, name, email); + var attributes = response.UserAttributes; - return customer; + var email = attributes.First(attr => attr.Name == "email").Value; + var name = attributes.First(attr => attr.Name == "name").Value; + var sub = attributes.First(attr => attr.Name == "sub").Value; + return new Customer(Guid.Parse(sub), cpf, name, email); } catch (UserNotFoundException) { + logger.LogWarning("Customer not found CPF {cpf}", cpf); return null; } catch (Exception ex) { - Console.WriteLine($"Error fetching user: {ex.Message}"); + logger.LogError(ex, "Error fetching user"); throw; } } @@ -54,6 +52,7 @@ public async Task CreateAsync(Customer customer) { try { + logger.LogInformation("Trying to create new customer"); var signUpResponse = await _cognitoClient.AdminCreateUserAsync(new AdminCreateUserRequest() { Username = customer.Cpf, @@ -65,58 +64,55 @@ public async Task CreateAsync(Customer customer) } }); - customer.Id = Guid.Parse(signUpResponse.User.Attributes.First(a=>a.Name is "sub").Value); + logger.LogInformation("Customer successfully created."); + customer.Id = Guid.Parse(signUpResponse.User.Attributes.First(a => a.Name is "sub").Value); return customer; } catch (UsernameExistsException ex) { - Console.WriteLine($"Error registering user: {ex.Message}"); + logger.LogWarning(ex, "There's already a customer using the provided CPF value"); throw new DomainException("There's already a customer using the provided CPF value."); } catch (Exception ex) { - Console.WriteLine($"Error registering user: {ex.Message}"); + logger.LogError(ex, "Error registering user"); throw; } } - private static string GenerateRandomPassword(int length) + public async Task FindByIdAsync(Guid id) { - using var rng = RandomNumberGenerator.Create(); - var characterSets = new[] + try { - "abcdefghijklmnopqrstuvwxyz", - "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "1234567890", - "!@#$%^&*()" - }; - var allChars = string.Concat(characterSets); - var passwordChars = new char[length]; - // Ensure the password contains at least one character from each character set - for (int i = 0; i < characterSets.Length && i < length; i++) + logger.LogInformation("Fetching user with Id {CustomerId}", id); + var response = await _cognitoClient.ListUsersAsync(new ListUsersRequest() + { + Filter = "sub=\"" + id + "\"", + UserPoolId = _userPoolId, + }); + + if (response.Users.Count > 0) + { + var attributes = response.Users[0].Attributes; + + var email = attributes.First(attr => attr.Name == "email").Value; + var name = attributes.First(attr => attr.Name == "name").Value; + var sub = attributes.First(attr => attr.Name == "sub").Value; + var cpf = response.Users[0].Username; + return new Customer(Guid.Parse(sub), cpf, name, email); + } + + return default; + } + catch (UserNotFoundException) { - passwordChars[i] = GetRandomChar(characterSets[i], rng); + logger.LogWarning("User not found."); + return null; } - // Fill the rest of the password with random characters - for (int i = characterSets.Length; i < length; i++) + catch (Exception ex) { - passwordChars[i] = GetRandomChar(allChars, rng); + logger.LogError(ex, "Error fetching user"); + throw; } - // Shuffle the password to ensure randomness - passwordChars = passwordChars.OrderBy(_ => GetRandomInt(rng, int.MaxValue)).ToArray(); - return new string(passwordChars); - } - private static char GetRandomChar(string chars, RandomNumberGenerator rng) - { - var bytes = new byte[4]; - rng.GetBytes(bytes); - var index = BitConverter.ToUInt32(bytes, 0) % chars.Length; - return chars[(int)index]; - } - private static int GetRandomInt(RandomNumberGenerator rng, int max) - { - var bytes = new byte[4]; - rng.GetBytes(bytes); - return (int)(BitConverter.ToUInt32(bytes, 0) % max); } } diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.csproj b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.csproj index 6c97a87..abdb002 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.csproj +++ b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.csproj @@ -15,6 +15,7 @@ + diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Domain/Entities/Order.cs b/src/FIAP.TechChallenge.ByteMeBurger.Domain/Entities/Order.cs index 894f110..908dc7c 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Domain/Entities/Order.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Domain/Entities/Order.cs @@ -99,6 +99,11 @@ public void SetPayment(PaymentId paymentId) Update(); } + public void SetCustomer(Customer customer) + { + Customer = customer; + } + public void ConfirmPayment() { if (Status != OrderStatus.PaymentPending) diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Domain/Interfaces/ICustomerRepository.cs b/src/FIAP.TechChallenge.ByteMeBurger.Domain/Interfaces/ICustomerRepository.cs index 807b375..00965df 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Domain/Interfaces/ICustomerRepository.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Domain/Interfaces/ICustomerRepository.cs @@ -7,4 +7,6 @@ public interface ICustomerRepository Task FindByCpfAsync(string cpf); Task CreateAsync(Customer customer); + + Task FindByIdAsync(Guid id); } diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/CustomerRepositoryDapper.cs b/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/CustomerRepositoryDapper.cs index 5523a3a..b0beb8e 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/CustomerRepositoryDapper.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/CustomerRepositoryDapper.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Diagnostics.CodeAnalysis; using Dapper; using FIAP.TechChallenge.ByteMeBurger.Domain.Entities; using FIAP.TechChallenge.ByteMeBurger.Domain.Interfaces; @@ -44,4 +45,11 @@ public async Task CreateAsync(Customer customer) return customer; } + + [ExcludeFromCodeCoverage] + [Obsolete("will be removed soon")] + public Task FindByIdAsync(Guid id) + { + throw new NotImplementedException(); + } } diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/InMemoryCustomerRepository.cs b/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/InMemoryCustomerRepository.cs index c1b182b..3662b90 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/InMemoryCustomerRepository.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/InMemoryCustomerRepository.cs @@ -19,4 +19,9 @@ public Task CreateAsync(Customer customer) _customers.Add(customer); return Task.FromResult(customer); } + + public Task FindByIdAsync(Guid id) + { + throw new NotImplementedException(); + } } diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/OrderRepositoryDapper.cs b/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/OrderRepositoryDapper.cs index 8d0e8e1..e379c8d 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/OrderRepositoryDapper.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Persistence/Repository/OrderRepositoryDapper.cs @@ -102,7 +102,7 @@ public async Task> GetAllAsync() } else { - order = new Order(orderListDto.Id, customerDto.FromDtoToEntity(), (OrderStatus)orderListDto.Status, + order = new Order(orderListDto.Id, new Customer(customerDto.Id), (OrderStatus)orderListDto.Status, new OrderTrackingCode(orderListDto.TrackingCode), orderListDto.Created, orderListDto.Updated); diff --git a/tests/FIAP.TechChallenge.ByteMeBurger.Application.Test/UseCases/Orders/GetOrderDetailsUseCaseTest.cs b/tests/FIAP.TechChallenge.ByteMeBurger.Application.Test/UseCases/Orders/GetOrderDetailsUseCaseTest.cs new file mode 100644 index 0000000..6c10a29 --- /dev/null +++ b/tests/FIAP.TechChallenge.ByteMeBurger.Application.Test/UseCases/Orders/GetOrderDetailsUseCaseTest.cs @@ -0,0 +1,83 @@ +using FIAP.TechChallenge.ByteMeBurger.Application.UseCases.Orders; +using FIAP.TechChallenge.ByteMeBurger.Domain.Interfaces; + +namespace FIAP.TechChallenge.ByteMeBurger.Application.Test.UseCases.Orders; + +[TestSubject(typeof(GetOrderDetailsUseCase))] +public class GetOrderDetailsUseCaseTest +{ + private readonly Mock _orderRepository; + private readonly Mock _customerRepository; + private readonly IGetOrderDetailsUseCase _useCase; + + public GetOrderDetailsUseCaseTest() + { + _orderRepository = new Mock(); + _customerRepository = new Mock(); + _useCase = new GetOrderDetailsUseCase(_orderRepository.Object, _customerRepository.Object); + } + + [Fact] + public async Task Execute_ShouldReturnOrder_WhenOrderExists() + { + // Arrange + var orderId = Guid.NewGuid(); + var order = new Order(orderId, null); + _orderRepository.Setup(r => r.GetAsync(orderId)).ReturnsAsync(order); + + // Act + var result = await _useCase.Execute(orderId); + + // Assert + using var scope = new AssertionScope(); + result.Should().NotBeNull(); + result.Id.Should().Be(orderId); + _customerRepository.Verify(c => c.FindByIdAsync(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task Execute_ShouldReturnNull_WhenOrderDoesNotExist() + { + // Arrange + var orderId = Guid.NewGuid(); + _orderRepository.Setup(r => r.GetAsync(orderId)).ReturnsAsync((Order?)null); + + // Act + var result = await _useCase.Execute(orderId); + + // Assert + result.Should().BeNull(); + _customerRepository.Verify(c => c.FindByIdAsync(It.IsAny()), Times.Never()); + } + + [Fact] + public async Task Execute_ShouldSetCustomer_WhenCustomerExists() + { + // Arrange + var orderId = Guid.NewGuid(); + var customerId = Guid.NewGuid(); + var order = new Order(orderId, new Customer(customerId)); + var customer = new Customer(customerId, "82227621095", "John Doe", "email@gmail.com"); + + _orderRepository.Setup(r => r.GetAsync(orderId)).ReturnsAsync(order); + _customerRepository.Setup(c => c.FindByIdAsync(customerId)).ReturnsAsync(customer); + + // Act + var result = await _useCase.Execute(orderId); + + // Assert + result.Should().NotBeNull(); + result.Customer.Should().NotBeNull(); + result.Customer.Should().Be(customer); + } + + [Fact] + public async Task Execute_ShouldReturnNull_WhenOrderIdIsEmpty() + { + // Act + var result = await _useCase.Execute(Guid.Empty); + + // Assert + result.Should().BeNull(); + } +} diff --git a/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoUserManagerTest.cs b/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoUserManagerTest.cs index e8a451d..781b17d 100644 --- a/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoUserManagerTest.cs +++ b/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoUserManagerTest.cs @@ -5,6 +5,7 @@ using FluentAssertions; using FluentAssertions.Execution; using JetBrains.Annotations; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -15,6 +16,7 @@ public class CognitoUserManagerTest { private readonly Mock _cognitoClientMock; private readonly CognitoUserManager _userManager; + private const string Cpf = "28642827041"; public CognitoUserManagerTest() { @@ -25,22 +27,22 @@ public CognitoUserManagerTest() { UserPoolId = "testPoolId", UserPoolClientId = "testClientId" }); mockFactory.Setup(f => f.CreateClient()).Returns(_cognitoClientMock.Object); - - _userManager = new CognitoUserManager(mockFactory.Object, settings); + _userManager = new CognitoUserManager(mockFactory.Object, Mock.Of>(), settings); } [Fact] public async Task FindByCpfAsync_ShouldReturnCustomer_WhenUserExists() { // Arrange - var cpf = "28642827041"; + var response = new AdminGetUserResponse { UserAttributes = [ new AttributeType { Name = "email", Value = "test@example.com" }, new AttributeType { Name = "name", Value = "Test User" }, - new AttributeType { Name = "sub", Value = Guid.NewGuid().ToString() } + new AttributeType { Name = "sub", Value = Guid.NewGuid().ToString() }, + new AttributeType { Name = "username", Value = Cpf } ] }; @@ -48,13 +50,13 @@ public async Task FindByCpfAsync_ShouldReturnCustomer_WhenUserExists() .ReturnsAsync(response); // Act - var result = await _userManager.FindByCpfAsync(cpf); + var result = await _userManager.FindByCpfAsync(Cpf); // Assert using (new AssertionScope()) { result.Should().NotBeNull(); - result.Cpf.Value.Should().Be(cpf); + result.Cpf.Value.Should().Be(Cpf); result.Email.Should().Be("test@example.com"); result.Name.Should().Be("Test User"); result.Id.Should().NotBeEmpty(); @@ -65,13 +67,11 @@ public async Task FindByCpfAsync_ShouldReturnCustomer_WhenUserExists() public async Task FindByCpfAsync_ShouldReturnNull_WhenUserNotFound() { // Arrange - const string cpf = "123456789"; - _cognitoClientMock.Setup(c => c.AdminGetUserAsync(It.IsAny(), default)) .ThrowsAsync(new UserNotFoundException("User not found")); // Act - var result = await _userManager.FindByCpfAsync(cpf); + var result = await _userManager.FindByCpfAsync(Cpf); // Assert result.Should().BeNull(); @@ -105,4 +105,70 @@ public async Task CreateAsync_ShouldReturnCustomer_WhenUserIsCreated() result.Id.Should().NotBeEmpty(); } } + + [Fact] + public async Task FindByIdAsync_UserExists_ReturnsCustomer() + { + // Arrange + var userId = Guid.NewGuid(); + var userAttributes = new List + { + new() { Name = "email", Value = "test@example.com" }, + new() { Name = "name", Value = "Test User" }, + new() { Name = "sub", Value = userId.ToString() } + }; + var user = new UserType { Attributes = userAttributes, Username = Cpf }; + var listUsersResponse = new ListUsersResponse { Users = new List { user } }; + + _cognitoClientMock.Setup(c => c.ListUsersAsync(It.IsAny(), default)) + .ReturnsAsync(listUsersResponse); + + // Act + var result = await _userManager.FindByIdAsync(userId); + + // Assert + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Id.Should().Be(userId); + result.Cpf.Value.Should().Be(Cpf); + result.Name.Should().Be("Test User"); + result.Email.Should().Be("test@example.com"); + } + } + + [Fact] + public async Task FindByIdAsync_UserNotFound_ReturnsNull() + { + // Arrange + var userId = Guid.NewGuid(); + var listUsersResponse = new ListUsersResponse { Users = new List() }; + + _cognitoClientMock.Setup(c => c.ListUsersAsync(It.IsAny(), default)) + .ReturnsAsync(listUsersResponse); + + // Act + var result = await _userManager.FindByIdAsync(userId); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task FindByIdAsync_ExceptionThrown_LogsErrorAndThrows() + { + // Arrange + var userId = Guid.NewGuid(); + var exception = new Exception("Test exception"); + + _cognitoClientMock.Setup(c => c.ListUsersAsync(It.IsAny(), default)) + .ThrowsAsync(exception); + + // Act & Assert + var func = () => _userManager.FindByIdAsync(userId); + await func + .Should() + .ThrowAsync() + .WithMessage(exception.Message); + } }