diff --git a/src/UserManager.API/Controllers/UserController.cs b/src/UserManager.API/Controllers/UserController.cs index 54ffc0f..51aba64 100644 --- a/src/UserManager.API/Controllers/UserController.cs +++ b/src/UserManager.API/Controllers/UserController.cs @@ -1,3 +1,5 @@ +using FluentValidation; +using FluentValidation.Results; using Microsoft.AspNetCore.Mvc; using UserManager.Application.Features.Users.Interfaces; using UserManager.Application.Features.Users.Requests; @@ -7,17 +9,31 @@ namespace UserManager.API.Controllers { [ApiController] [Route("api/[controller]")] - public class UserController(ILogger logger, IUserService userService) : ControllerBase + public class UserController(ILogger logger, IUserService userService, IValidator createUserValidator) : ControllerBase { private readonly ILogger _logger = logger; private readonly IUserService _userService = userService; + private readonly IValidator _createUserValidator = createUserValidator; + [HttpPost(Name = "CreateUser")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> CreateUserAsync(CreateUserRequestDto request) { _logger.LogInformation("Creating user with first name {FirstName} and last name {LastName}", request.FirstName, request.LastName); - CreateUserResponseDto response = await _userService.CreateUserAsync(request); - return Ok(response); + + ValidationResult? validationResult = await _createUserValidator.ValidateAsync(request); + + if (!validationResult.IsValid) + { + return BadRequest(validationResult.Errors); + } + else + { + CreateUserResponseDto response = await _userService.CreateUserAsync(request); + return Ok(response); + } } } } diff --git a/src/UserManager.Application/DependancyInjection.cs b/src/UserManager.Application/DependancyInjection.cs index 75dc564..53ec1c4 100644 --- a/src/UserManager.Application/DependancyInjection.cs +++ b/src/UserManager.Application/DependancyInjection.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using UserManager.Application.Features.Users.Interfaces; +using UserManager.Application.Features.Users.Requests; using UserManager.Application.Features.Users.Services; +using FluentValidation; namespace UserManager.Application { @@ -9,6 +11,8 @@ namespace UserManager.Application public static void AddApplication(this IServiceCollection services) { services.AddScoped(); + + services.AddValidatorsFromAssemblyContaining(); } } } diff --git a/src/UserManager.Application/Features/Users/Requests/CreateUserRequestDto.cs b/src/UserManager.Application/Features/Users/Requests/CreateUserRequestDto.cs index 964ea53..442e422 100644 --- a/src/UserManager.Application/Features/Users/Requests/CreateUserRequestDto.cs +++ b/src/UserManager.Application/Features/Users/Requests/CreateUserRequestDto.cs @@ -1,10 +1,19 @@ -namespace UserManager.Application.Features.Users.Requests +using FluentValidation; + +namespace UserManager.Application.Features.Users.Requests { public class CreateUserRequestDto { public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; - public int? FavouriteRestaurantId { get; set; } - public string? FavouriteRestaurantName { get; set; } + } + + public class CreateUserRequestDtoValidator : AbstractValidator + { + public CreateUserRequestDtoValidator() + { + RuleFor(x => x.FirstName).NotEmpty().WithMessage("FirstName is required."); + RuleFor(x => x.LastName).NotEmpty().WithMessage("LastName is required."); + } } } diff --git a/src/UserManager.Application/Features/Users/Services/UserService.cs b/src/UserManager.Application/Features/Users/Services/UserService.cs index 1dc7431..f98f296 100644 --- a/src/UserManager.Application/Features/Users/Services/UserService.cs +++ b/src/UserManager.Application/Features/Users/Services/UserService.cs @@ -6,7 +6,7 @@ using UserManager.Domain.Entities; namespace UserManager.Application.Features.Users.Services { - class UserService(IUnitOfWork unitOfWork) : IUserService + public class UserService(IUnitOfWork unitOfWork) : IUserService { private readonly IUnitOfWork _unitOfWork = unitOfWork; @@ -18,6 +18,7 @@ namespace UserManager.Application.Features.Users.Services { FirstName = request.FirstName, LastName = request.LastName, + CreateAtUtc = DateTime.UtcNow, }; await _unitOfWork.UserRepository.CreateAsync(user); diff --git a/src/UserManager.Application/UserManager.Application.csproj b/src/UserManager.Application/UserManager.Application.csproj index 4084818..5a9448f 100644 --- a/src/UserManager.Application/UserManager.Application.csproj +++ b/src/UserManager.Application/UserManager.Application.csproj @@ -7,10 +7,14 @@ - + + + + + diff --git a/src/UserManager.Domain/Entities/User.cs b/src/UserManager.Domain/Entities/User.cs index 2e003af..19d1f39 100644 --- a/src/UserManager.Domain/Entities/User.cs +++ b/src/UserManager.Domain/Entities/User.cs @@ -1,15 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace UserManager.Domain.Entities +namespace UserManager.Domain.Entities { public class User { public int UserId { get; set; } public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; + + public bool IsDeleted { get; set; } + + public DateTime CreateAtUtc { get; set; } + public DateTime? UpdatedAtUtc { get; set; } + public DateTime? DeletedAtUtc { get; set; } } -} +} \ No newline at end of file diff --git a/src/UserManager.sln b/src/UserManager.sln index 6f7d91e..d7e05e9 100644 --- a/src/UserManager.sln +++ b/src/UserManager.sln @@ -7,15 +7,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{74E412ED-9D1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5B673FE8-32DA-4CD3-8394-1BD8E1275270}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManager.API", "UserManager.API\UserManager.API.csproj", "{A9A6D6F8-424B-4555-95D9-ECE60837B800}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManager.API", "UserManager.API\UserManager.API.csproj", "{A9A6D6F8-424B-4555-95D9-ECE60837B800}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManager.Domain", "UserManager.Domain\UserManager.Domain.csproj", "{17851242-59E3-4790-9FD8-32B848BBAB3D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManager.Domain", "UserManager.Domain\UserManager.Domain.csproj", "{17851242-59E3-4790-9FD8-32B848BBAB3D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManager.Infrastructure", "UserManager.Infrastructure\UserManager.Infrastructure.csproj", "{A85D13CB-884E-439D-AAE7-C4C4C2511FDE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManager.Infrastructure", "UserManager.Infrastructure\UserManager.Infrastructure.csproj", "{A85D13CB-884E-439D-AAE7-C4C4C2511FDE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManager.Application", "UserManager.Application\UserManager.Application.csproj", "{C7215859-A216-4527-A192-7B8F57E50BB4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManager.Application", "UserManager.Application\UserManager.Application.csproj", "{C7215859-A216-4527-A192-7B8F57E50BB4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManager.Application.UnitTests", "..\tests\UserManager.Application.Tests\UserManager.Application.UnitTests.csproj", "{B0F63D11-6933-4D87-B0D8-6DD7DFC5A23C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManager.Application.UnitTests", "..\tests\UserManager.Application.Tests\UserManager.Application.UnitTests.csproj", "{B0F63D11-6933-4D87-B0D8-6DD7DFC5A23C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManager.Application.IntegrationTests", "..\tests\UserManager.Application.IntegrationTests\UserManager.Application.IntegrationTests.csproj", "{2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -43,6 +45,10 @@ Global {B0F63D11-6933-4D87-B0D8-6DD7DFC5A23C}.Debug|Any CPU.Build.0 = Debug|Any CPU {B0F63D11-6933-4D87-B0D8-6DD7DFC5A23C}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0F63D11-6933-4D87-B0D8-6DD7DFC5A23C}.Release|Any CPU.Build.0 = Release|Any CPU + {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -53,6 +59,7 @@ Global {A85D13CB-884E-439D-AAE7-C4C4C2511FDE} = {74E412ED-9D1B-4597-BBFD-05623C814F98} {C7215859-A216-4527-A192-7B8F57E50BB4} = {74E412ED-9D1B-4597-BBFD-05623C814F98} {B0F63D11-6933-4D87-B0D8-6DD7DFC5A23C} = {5B673FE8-32DA-4CD3-8394-1BD8E1275270} + {2EDF27EB-CC43-4F38-86A5-BDC1D6974BA5} = {5B673FE8-32DA-4CD3-8394-1BD8E1275270} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6E7F90F4-4A09-4CDB-8653-6E094E164853} diff --git a/tests/UserManager.Application.IntegrationTests/Features/Users/UserServiceIntegrationTests.cs b/tests/UserManager.Application.IntegrationTests/Features/Users/UserServiceIntegrationTests.cs new file mode 100644 index 0000000..15b08b0 --- /dev/null +++ b/tests/UserManager.Application.IntegrationTests/Features/Users/UserServiceIntegrationTests.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using UserManager.Application.Features.Users.Requests; +using UserManager.Application.Features.Users.Responses; +using UserManager.Application.Features.Users.Services; +using UserManager.Domain.Entities; +using UserManager.Infrastructure; + +namespace UserManager.Application.IntegrationTests.Features.Users +{ + [TestClass] + public class UserServiceIntegrationTests + { + + private UserManagerContext _context = null!; + + [TestInitialize] + public void Initialize() + { + DbContextOptions options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + _context = new UserManagerContext(options); + } + + [TestCleanup] + public void Cleanup() + { + _context.Dispose(); + _context = null!; + } + + [TestMethod] + public async Task CreateUserAsync_ShouldAddUserToDatabase_WhenCreateUserCalled() + { + // Arrange + var request = new CreateUserRequestDto() + { + FirstName = "John", + LastName = "Doe", + }; + + UnitOfWork unitOfWork = new(_context); + UserService userService = new(unitOfWork); + + // Act + CreateUserResponseDto responseDto = await userService.CreateUserAsync(request); + + // Assert + User user = _context.Users.First(); + Assert.AreEqual(1, _context.Users.Count()); + Assert.AreEqual("John", user.FirstName); + Assert.AreEqual("John", responseDto.FirstName); + Assert.AreEqual(1, user.UserId); + Assert.AreEqual(1, responseDto.UserId); + Assert.IsTrue(user.CreateAtUtc > DateTime.UtcNow.AddSeconds(-1)); + Assert.IsTrue(user.CreateAtUtc < DateTime.UtcNow.AddSeconds(1)); + Assert.IsNull(user.UpdatedAtUtc); + Assert.IsNull(user.DeletedAtUtc); + Assert.IsFalse(user.IsDeleted); + } + } +} diff --git a/tests/UserManager.Application.IntegrationTests/UnitTest1.cs b/tests/UserManager.Application.IntegrationTests/UnitTest1.cs new file mode 100644 index 0000000..f227925 --- /dev/null +++ b/tests/UserManager.Application.IntegrationTests/UnitTest1.cs @@ -0,0 +1,11 @@ +namespace UserManager.Application.IntegrationTests +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} \ No newline at end of file diff --git a/tests/UserManager.Application.IntegrationTests/UserManager.Application.IntegrationTests.csproj b/tests/UserManager.Application.IntegrationTests/UserManager.Application.IntegrationTests.csproj new file mode 100644 index 0000000..88b2a59 --- /dev/null +++ b/tests/UserManager.Application.IntegrationTests/UserManager.Application.IntegrationTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/tests/UserManager.Application.Tests/Features/Users/CreateUserRequestDtoValidationUnitTests.cs b/tests/UserManager.Application.Tests/Features/Users/CreateUserRequestDtoValidationUnitTests.cs new file mode 100644 index 0000000..aa78279 --- /dev/null +++ b/tests/UserManager.Application.Tests/Features/Users/CreateUserRequestDtoValidationUnitTests.cs @@ -0,0 +1,46 @@ +using FluentValidation.Results; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UserManager.Application.Features.Users.Requests; + +namespace UserManager.Application.UnitTests.Features.Users +{ + [TestClass] + public class CreateUserRequestDtoValidationUnitTests + { + [TestMethod] + public void CreateUserRequestDtoValidator_WhenFirstNameIsEmpty_ShouldReturnValidationError() + { + // Arrange + CreateUserRequestDto request = new() + { + FirstName = string.Empty, + LastName = "Doe" + }; + + // Act + ValidationResult validationResult = new CreateUserRequestDtoValidator().Validate(request); + + // Assert + Assert.IsFalse(validationResult.IsValid); + Assert.AreEqual("FirstName is required.", validationResult.Errors[0].ErrorMessage); + } + + [TestMethod] + public void CreateUserRequestDtoValidator_WhenLastNameIsEmpty_ShouldReturnValidationError() + { + // Arrange + CreateUserRequestDto request = new() + { + FirstName = "John", + LastName = string.Empty + }; + + // Act + ValidationResult validationResult = new CreateUserRequestDtoValidator().Validate(request); + + // Assert + Assert.IsFalse(validationResult.IsValid); + Assert.AreEqual("LastName is required.", validationResult.Errors[0].ErrorMessage); + } + } +} diff --git a/tests/UserManager.Application.Tests/Features/Users/UserServiceUnitTests.cs b/tests/UserManager.Application.Tests/Features/Users/UserServiceUnitTests.cs new file mode 100644 index 0000000..0273043 --- /dev/null +++ b/tests/UserManager.Application.Tests/Features/Users/UserServiceUnitTests.cs @@ -0,0 +1,37 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using UserManager.Application.Features.Users.Requests; +using UserManager.Application.Interfaces; +using UserManager.Application.Features.Users.Services; +using UserManager.Application.Features.Users.Responses; +using UserManager.Domain.Entities; + +namespace UserManager.Application.UnitTests.Features.Users +{ + [TestClass] + public class UserServiceUnitTests + { + [TestMethod] + public async Task CreateUserAsync_ShouldCallAddAndSave_WhenCreateUserCalled() + { + // Arrange + Mock unitOfWork = new(); + Mock userRepository = new(); + unitOfWork.SetupGet(u => u.UserRepository).Returns(userRepository.Object); + var request = new CreateUserRequestDto() + { + FirstName = "John", + LastName = "Doe", + }; + + UserService userService = new(unitOfWork.Object); + + // Act + CreateUserResponseDto responseDto = await userService.CreateUserAsync(request); + + // Assert + unitOfWork.Verify(u => u.UserRepository.CreateAsync(It.IsAny()), Times.Once); + unitOfWork.Verify(u => u.SaveChangesAsync(), Times.Once); + } + } +} diff --git a/tests/UserManager.Application.Tests/UserManager.Application.UnitTests.csproj b/tests/UserManager.Application.Tests/UserManager.Application.UnitTests.csproj index fa71b7a..52b46dc 100644 --- a/tests/UserManager.Application.Tests/UserManager.Application.UnitTests.csproj +++ b/tests/UserManager.Application.Tests/UserManager.Application.UnitTests.csproj @@ -4,6 +4,18 @@ net8.0 enable enable + true + + + + + + + + + + +