diff --git a/tests/Application.UnitTest/Common/Results/ResultTests.cs b/tests/Application.UnitTest/Common/Results/ResultTests.cs new file mode 100644 index 0000000..ece8bec --- /dev/null +++ b/tests/Application.UnitTest/Common/Results/ResultTests.cs @@ -0,0 +1,64 @@ +using Application.Common.Results; + +namespace Application.UnitTest.Common.Results; + +public class ResultTests +{ + [Fact] + public void Success_ShouldReturnResultWithIsSuccessTrue() + { + var result = Result.Success(); + Assert.True(result.IsSuccess); + Assert.False(result.IsFailure); + Assert.Equal(ErrorTypeConstant.None, result.Error.Code); + } + + [Fact] + public void Success_WithValue_ShouldReturnResultWithValue() + { + var result = Result.Success("test-value"); + Assert.True(result.IsSuccess); + Assert.Equal("test-value", result.Value); + } + + [Fact] + public void Failure_ShouldReturnResultWithIsFailureTrue() + { + var error = new Error("TestError", "Something went wrong"); + var result = Result.Failure(error); + Assert.True(result.IsFailure); + Assert.False(result.IsSuccess); + Assert.Equal("TestError", result.Error.Code); + Assert.Equal("Something went wrong", result.Error.Message); + } + + [Fact] + public void Failure_WithValueType_ShouldReturnTypedResult() + { + var error = new Error("NotFound", "Item not found"); + var result = Result.Failure(error); + Assert.True(result.IsFailure); + Assert.Throws(() => result.Value); + } + + [Fact] + public void Constructor_ShouldThrow_WhenSuccessWithError() + { + var error = new Error("SomeError", "message"); + Assert.Throws(() => new ResultTestCase(true, error)); + } + + [Fact] + public void Constructor_ShouldThrow_WhenFailureWithNoError() + { + var noneError = new Error("None", string.Empty); + Assert.Throws(() => new ResultTestCase(false, noneError)); + } +} + +public class ResultTestCase : Result +{ + public ResultTestCase(bool isSuccess, Error error) : base(isSuccess, error) + { + } +} diff --git a/tests/Application.UnitTest/DTOs/PagedResultTests.cs b/tests/Application.UnitTest/DTOs/PagedResultTests.cs new file mode 100644 index 0000000..21bd50d --- /dev/null +++ b/tests/Application.UnitTest/DTOs/PagedResultTests.cs @@ -0,0 +1,110 @@ +using Application.DTOs; + +namespace Application.UnitTest.DTOs; + +public class PagedResultTests +{ + [Fact] + public void TotalPages_ShouldCalculateCorrectly_WhenExactDivision() + { + var paged = new PagedResult + { + Items = ["a", "b", "c", "d", "e"], + TotalCount = 5, + PageNumber = 1, + PageSize = 5 + }; + Assert.Equal(1, paged.TotalPages); + } + + [Fact] + public void TotalPages_ShouldRoundUp_WhenNotExactDivision() + { + var paged = new PagedResult + { + Items = ["a", "b", "c"], + TotalCount = 10, + PageNumber = 1, + PageSize = 3 + }; + Assert.Equal(4, paged.TotalPages); + } + + [Fact] + public void TotalPages_ShouldBeZero_WhenTotalCountIsZero() + { + var paged = new PagedResult + { + Items = [], + TotalCount = 0, + PageNumber = 1, + PageSize = 10 + }; + Assert.Equal(0, paged.TotalPages); + } + + [Fact] + public void HasPreviousPage_ShouldBeFalse_WhenOnFirstPage() + { + var paged = new PagedResult + { + Items = [], + TotalCount = 20, + PageNumber = 1, + PageSize = 10 + }; + Assert.False(paged.HasPreviousPage); + } + + [Fact] + public void HasPreviousPage_ShouldBeTrue_WhenPastFirstPage() + { + var paged = new PagedResult + { + Items = [], + TotalCount = 20, + PageNumber = 2, + PageSize = 10 + }; + Assert.True(paged.HasPreviousPage); + } + + [Fact] + public void HasNext_ShouldBeFalse_WhenOnLastPage() + { + var paged = new PagedResult + { + Items = [], + TotalCount = 20, + PageNumber = 2, + PageSize = 10 + }; + Assert.False(paged.HasNext); + } + + [Fact] + public void HasNext_ShouldBeTrue_WhenMorePagesExist() + { + var paged = new PagedResult + { + Items = [], + TotalCount = 20, + PageNumber = 1, + PageSize = 10 + }; + Assert.True(paged.HasNext); + } + + [Fact] + public void HasNext_ShouldBeFalse_WhenOnlyOnePage() + { + var paged = new PagedResult + { + Items = [], + TotalCount = 3, + PageNumber = 1, + PageSize = 10 + }; + Assert.False(paged.HasNext); + } +} diff --git a/tests/Application.UnitTest/Services/AuthenticationServiceTests.cs b/tests/Application.UnitTest/Services/AuthenticationServiceTests.cs index 7bd5a95..c39e31e 100644 --- a/tests/Application.UnitTest/Services/AuthenticationServiceTests.cs +++ b/tests/Application.UnitTest/Services/AuthenticationServiceTests.cs @@ -134,6 +134,28 @@ public class AuthenticationServiceTests Assert.Equal(AuthError.UserNotFound, result.Error); } + [Fact] + public async Task LoginAsync_ShouldReturnFailure_WhenPasswordIsWrong() + { + var password = "ValidP@ss1!"; + var user = new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + Password = new PasswordHasher().HashPassword( + new User { Username = "testuser", Email = "test@example.com", Password = password }, + password) + }; + var request = new LoginRequest(user.Email, "WrongPassword1!"); + _userRepository.GetUserByEmailAsync(request.Email).Returns(user); + + var result = await _sut.LoginAsync(request); + + Assert.True(result.IsFailure); + Assert.Equal(AuthError.InvalidPassword, result.Error); + } + [Fact] public async Task RefreshTokensAsync_ShouldReturnSuccess_WhenRefreshTokenIsValid() { diff --git a/tests/Application.UnitTest/Services/EmailServiceTests.cs b/tests/Application.UnitTest/Services/EmailServiceTests.cs new file mode 100644 index 0000000..2977357 --- /dev/null +++ b/tests/Application.UnitTest/Services/EmailServiceTests.cs @@ -0,0 +1,60 @@ +using Application.Models; +using Application.Services; +using MailKit.Net.Smtp; +using Microsoft.Extensions.Configuration; +using MimeKit; +using NSubstitute; + +namespace Application.UnitTest.Services; + +public class EmailServiceTests +{ + private readonly IConfiguration _configuration = Substitute.For(); + private readonly ISmtpClient _smtpClient = Substitute.For(); + private readonly EmailService _sut; + + public EmailServiceTests() + { + _configuration["EmailSettings:From"].Returns("sender@gmail.com"); + _configuration["EmailSettings:SmtpServer"].Returns("smtp.gmail.com"); + _configuration["EmailSettings:Password"].Returns("app-password"); + + _sut = new EmailService(_configuration, () => _smtpClient); + } + + [Fact] + public void SendEmailAsync_ShouldConnectAndAuthenticate() + { + var request = new EmailRequest("recipient@example.com", "Test Subject", "

Hello

"); + + _sut.SendEmailAsync(request); + + _smtpClient.Received(1).Connect("smtp.gmail.com", 465, true); + _smtpClient.Received(1).Authenticate("sender@gmail.com", "app-password"); + _smtpClient.Received(1).Send(Arg.Any()); + _smtpClient.Received(1).Disconnect(true); + } + + [Fact] + public void SendEmailAsync_ShouldSendMessageWithCorrectDetails() + { + var request = new EmailRequest("recipient@example.com", "Welcome!", "

Hi

"); + + _sut.SendEmailAsync(request); + + _smtpClient.Received(1).Send(Arg.Is(msg => + msg.To.Mailboxes.Any(m => m.Address == "recipient@example.com") && + msg.Subject == "Welcome!")); + } + + [Fact] + public void SendEmailAsync_ShouldDisposeClient_AfterSending() + { + var request = new EmailRequest("recipient@example.com", "Subject", "Body"); + + _sut.SendEmailAsync(request); + + _smtpClient.Received(1).Disconnect(true); + _smtpClient.Received(1).Dispose(); + } +} diff --git a/tests/Application.UnitTest/Services/JwtServiceTests.cs b/tests/Application.UnitTest/Services/JwtServiceTests.cs new file mode 100644 index 0000000..bf98d55 --- /dev/null +++ b/tests/Application.UnitTest/Services/JwtServiceTests.cs @@ -0,0 +1,170 @@ +using System.IdentityModel.Tokens.Jwt; +using Application.Services; +using Domain.Entities; +using Domain.Interface; +using Microsoft.Extensions.Configuration; +using NSubstitute; + +namespace Application.UnitTest.Services; + +public class JwtServiceTests +{ + private readonly IConfiguration _configuration = Substitute.For(); + private readonly IUserRepository _userRepository = Substitute.For(); + private readonly IUnitOfWork _unitOfWork = Substitute.For(); + private readonly JwtService _sut; + + public JwtServiceTests() + { + var section = Substitute.For(); + section.Value.Returns("veryveryveryveryveryveryverysecretkey"); + _configuration.GetSection("Jwt:Key").Returns(section); + + _configuration["Jwt:Key"].Returns("veryveryveryveryveryveryverysecretkey"); + _configuration["Jwt:Issuer"].Returns("https://localhost:7091"); + _configuration["Jwt:Audience"].Returns("http://localhost:5184"); + + _sut = new JwtService(_configuration, _userRepository, _unitOfWork); + } + + [Fact] + public async Task GenerateTokenAsync_ShouldReturnToken_WhenUserIsValid() + { + var user = new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + Password = "hash" + }; + _userRepository.GetUserRolesByEmailAsync(user.Email) + .Returns(["User"]); + + var token = await _sut.GenerateTokenAsync(user); + + Assert.NotNull(token); + Assert.NotEmpty(token); + + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadJwtToken(token); + + Assert.Equal("https://localhost:7091", jwtToken.Issuer); + Assert.Contains(jwtToken.Claims, c => c.Type == "email" && c.Value == user.Email); + Assert.Contains(jwtToken.Claims, c => c.Type == "UserId" && c.Value == "1"); + Assert.Contains(jwtToken.Claims, c => c.Type == "username" && c.Value == user.Username); + Assert.Contains(jwtToken.Claims, c => c.Type == "role" && c.Value == "User"); + } + + [Fact] + public async Task GenerateTokenAsync_ShouldIncludeAllRoles() + { + var user = new User + { + Id = 2, + Username = "adminuser", + Email = "admin@example.com", + Password = "hash" + }; + _userRepository.GetUserRolesByEmailAsync(user.Email) + .Returns(["Admin", "User"]); + + var token = await _sut.GenerateTokenAsync(user); + + var handler = new JwtSecurityTokenHandler(); + var jwtToken = handler.ReadJwtToken(token); + var roleClaims = jwtToken.Claims.Where(c => c.Type == "role").Select(c => c.Value).ToList(); + + Assert.Contains("Admin", roleClaims); + Assert.Contains("User", roleClaims); + } + + [Fact] + public async Task GenerateAndSaveRefreshTokenAsync_ShouldUpdateUserAndCommit() + { + var user = new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + Password = "hash" + }; + + var refreshToken = await _sut.GenerateAndSaveRefreshTokenAsync(user); + + Assert.NotNull(refreshToken); + Assert.NotEmpty(refreshToken); + Assert.Equal(refreshToken, user.RefreshToken); + Assert.NotNull(user.RefreshTokenExpiryTime); + Assert.True(user.RefreshTokenExpiryTime > DateTime.UtcNow); + _userRepository.Received(1).Update(user); + await _unitOfWork.Received(1).CommitAsync(); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_ShouldReturnNull_WhenUserNotFound() + { + _userRepository.GetUserByIdAsync(99).Returns((User?)null); + + var result = await _sut.ValidateRefreshTokenAsync(99, "some-token"); + + Assert.Null(result); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_ShouldReturnNull_WhenTokenMismatch() + { + var user = new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + Password = "hash", + RefreshToken = "stored-token", + RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(1) + }; + _userRepository.GetUserByIdAsync(1).Returns(user); + + var result = await _sut.ValidateRefreshTokenAsync(1, "wrong-token"); + + Assert.Null(result); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_ShouldReturnNull_WhenTokenExpired() + { + var user = new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + Password = "hash", + RefreshToken = "expired-token", + RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(-1) + }; + _userRepository.GetUserByIdAsync(1).Returns(user); + + var result = await _sut.ValidateRefreshTokenAsync(1, "expired-token"); + + Assert.Null(result); + } + + [Fact] + public async Task ValidateRefreshTokenAsync_ShouldReturnUser_WhenTokenValid() + { + var user = new User + { + Id = 1, + Username = "testuser", + Email = "test@example.com", + Password = "hash", + RefreshToken = "valid-token", + RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(1) + }; + _userRepository.GetUserByIdAsync(1).Returns(user); + + var result = await _sut.ValidateRefreshTokenAsync(1, "valid-token"); + + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } +}