From 25baaa0a67ef9c88af6079b88dec308ef1bb5838 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Mon, 13 Mar 2023 18:36:59 +0200 Subject: [PATCH] Added unit tests --- .../Services/UserService.cs | 11 + .../DependencyInjectionExtensions.cs | 14 +- .../CommandHandlerTests.cs | 66 ++++++ .../Fixtures/DependencyInjectionFixture.cs | 24 ++- .../HashingServiceTests.cs | 4 +- .../LocalSqliteDatabaseTests.cs | 4 +- .../TokenServiceTests.cs | 4 +- .../TokenStoreTests.cs | 4 +- .../UserServiceTests.cs | 197 ++++++++++++++++++ 9 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 test/UnitTests/Tuitio.Application.Tests/CommandHandlerTests.cs create mode 100644 test/UnitTests/Tuitio.Application.Tests/UserServiceTests.cs diff --git a/src/Tuitio.Application/Services/UserService.cs b/src/Tuitio.Application/Services/UserService.cs index df57c5d..4c97fda 100644 --- a/src/Tuitio.Application/Services/UserService.cs +++ b/src/Tuitio.Application/Services/UserService.cs @@ -31,6 +31,8 @@ namespace Tuitio.Application.Services public async Task Login(string userName, string password) { + ValidateCredentials(userName, password); + var passwordHash = _hashingService.HashSha256(password); var user = await _userRepository.GetUser(userName, passwordHash); if (user == null) @@ -69,6 +71,15 @@ namespace Tuitio.Application.Services return token; } + private void ValidateCredentials(string userName, string password) + { + if (string.IsNullOrEmpty(userName)) + throw new ArgumentException($"Value cannot be null or empty string.", nameof(userName)); + + if (string.IsNullOrEmpty(password)) + throw new ArgumentException($"Value cannot be null or empty string.", nameof(password)); + } + private bool ValidateUser(AppUser user) { if (user == null) diff --git a/src/Tuitio.Domain.Data/DependencyInjectionExtensions.cs b/src/Tuitio.Domain.Data/DependencyInjectionExtensions.cs index 36e3dd6..cec55e6 100644 --- a/src/Tuitio.Domain.Data/DependencyInjectionExtensions.cs +++ b/src/Tuitio.Domain.Data/DependencyInjectionExtensions.cs @@ -1,20 +1,24 @@ // Copyright (c) 2020 Tudor Stanciu +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Tuitio.Domain.Data.DbContexts; using Tuitio.Domain.Data.Repositories; using Tuitio.Domain.Repositories; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.EntityFrameworkCore; namespace Tuitio.Domain.Data { public static class DependencyInjectionExtensions { - public static void AddDataAccess(this IServiceCollection services) + public static void AddDataAccessServices(this IServiceCollection services) { services.AddScoped(); - + } + + public static void AddDataAccess(this IServiceCollection services) + { + services.AddDataAccessServices(); services .AddDbContextPool( (serviceProvider, options) => diff --git a/test/UnitTests/Tuitio.Application.Tests/CommandHandlerTests.cs b/test/UnitTests/Tuitio.Application.Tests/CommandHandlerTests.cs new file mode 100644 index 0000000..df88b34 --- /dev/null +++ b/test/UnitTests/Tuitio.Application.Tests/CommandHandlerTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2020 Tudor Stanciu + +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Tuitio.Application.CommandHandlers; +using Tuitio.Application.Tests.Fixtures; +using Tuitio.PublishedLanguage.Constants; +using Xunit; + +namespace Tuitio.Application.Tests +{ + public class CommandHandlerTests : IClassFixture, IDisposable + { + private readonly IServiceScope _tuitioScope; + private readonly IMediator _mediator; + + public CommandHandlerTests(DependencyInjectionFixture fixture) + { + _tuitioScope = fixture.ServiceProvider.GetRequiredService().CreateScope(); + _mediator = _tuitioScope.ServiceProvider.GetRequiredService(); + } + + public void Dispose() + { + _tuitioScope.Dispose(); + } + + [Fact] + public async Task AccountLoginHandler_Handle_Success() + { + // Arrange + var userName = "tuitio.user"; + var password = "pass123"; + var command = new AccountLoginHandler.Command(userName, password); + + // Act + var result = await _mediator.Send(command); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Result); + Assert.Null(result.Error); + Assert.NotEmpty(result.Result.Token); + Assert.True(result.Result.ExpiresIn > 0, "Token expiration must be a positive number."); + } + + [Fact] + public async Task AccountLoginHandler_Handle_Failed() + { + // Arrange + var userName = "tuitio.user"; + var password = "wrong_password"; + var command = new AccountLoginHandler.Command(userName, password); + + // Act + var result = await _mediator.Send(command); + + // Assert + Assert.NotNull(result); + Assert.Null(result.Result); + Assert.NotNull(result.Error); + Assert.NotEmpty(result.Error); + Assert.Equal(EnvelopeError.BAD_CREDENTIALS, result.Error); + } + } +} diff --git a/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs b/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs index 1941a57..a7a0d6f 100644 --- a/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs +++ b/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs @@ -1,4 +1,6 @@ -using MediatR; +// Copyright (c) 2020 Tudor Stanciu + +using MediatR; using MediatR.Pipeline; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; @@ -6,6 +8,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Data.Common; +using Tuitio.Domain.Data; using Tuitio.Domain.Data.DbContexts; using Xunit; @@ -65,6 +68,7 @@ namespace Tuitio.Application.Tests.Fixtures options.EnableDetailedErrors(); }); + services.AddDataAccessServices(); services.AddApplicationServices(); var provider = services.BuildServiceProvider(); @@ -92,6 +96,7 @@ namespace Tuitio.Application.Tests.Fixtures { using var dbContext = ServiceProvider.GetRequiredService(); + #region UserStatus dbContext.UserStatuses.AddRange(new Domain.Entities.UserStatus[] { new Domain.Entities.UserStatus() @@ -113,6 +118,23 @@ namespace Tuitio.Application.Tests.Fixtures StatusName = "Blocked" } }); + #endregion + + #region Users + dbContext.Users.Add(new Domain.Entities.AppUser() + { + UserId = 1, + UserName = "tuitio.user", + Password = "9B8769A4A742959A2D0298C36FB70623F2DFACDA8436237DF08D8DFD5B37374C", // pass123 + FirstName = "Tuitio", + LastName = "User", + Email = "tuitio.user@tuitio.lab", + SecurityStamp = "A93650FF-1FC4-4999-BAB6-3EEB174F6892", + StatusId = 1, + CreationDate = DateTime.Now, + FailedLoginAttempts = 0 + }); + #endregion dbContext.SaveChanges(); } diff --git a/test/UnitTests/Tuitio.Application.Tests/HashingServiceTests.cs b/test/UnitTests/Tuitio.Application.Tests/HashingServiceTests.cs index 055e286..f53f8d2 100644 --- a/test/UnitTests/Tuitio.Application.Tests/HashingServiceTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/HashingServiceTests.cs @@ -1,4 +1,6 @@ -using Tuitio.Application.Services; +// Copyright (c) 2020 Tudor Stanciu + +using Tuitio.Application.Services; using Xunit; namespace Tuitio.Application.Tests diff --git a/test/UnitTests/Tuitio.Application.Tests/LocalSqliteDatabaseTests.cs b/test/UnitTests/Tuitio.Application.Tests/LocalSqliteDatabaseTests.cs index 99722ed..c76e6ab 100644 --- a/test/UnitTests/Tuitio.Application.Tests/LocalSqliteDatabaseTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/LocalSqliteDatabaseTests.cs @@ -1,4 +1,6 @@ -using Microsoft.EntityFrameworkCore; +// Copyright (c) 2020 Tudor Stanciu + +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tuitio.Application.Tests.Fixtures; using Tuitio.Domain.Data.DbContexts; diff --git a/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs b/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs index e60cb60..4559431 100644 --- a/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; +// Copyright (c) 2020 Tudor Stanciu + +using Microsoft.Extensions.DependencyInjection; using Tuitio.Application.Services.Abstractions; using Tuitio.Application.Tests.Fixtures; using Tuitio.Domain.Entities; diff --git a/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs b/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs index 2353592..273ed12 100644 --- a/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs +++ b/test/UnitTests/Tuitio.Application.Tests/TokenStoreTests.cs @@ -1,4 +1,6 @@ -using Tuitio.Application.Stores; +// Copyright (c) 2020 Tudor Stanciu + +using Tuitio.Application.Stores; using Tuitio.Domain.Models; using Xunit; diff --git a/test/UnitTests/Tuitio.Application.Tests/UserServiceTests.cs b/test/UnitTests/Tuitio.Application.Tests/UserServiceTests.cs new file mode 100644 index 0000000..59dab95 --- /dev/null +++ b/test/UnitTests/Tuitio.Application.Tests/UserServiceTests.cs @@ -0,0 +1,197 @@ +// Copyright (c) 2020 Tudor Stanciu + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tuitio.Application.Services.Abstractions; +using Tuitio.Application.Tests.Fixtures; +using Tuitio.Domain.Data.DbContexts; +using Tuitio.Domain.Models.Account; +using Xunit; + +namespace Tuitio.Application.Tests +{ + public class UserServiceTests : IClassFixture, IDisposable + { + private readonly IServiceScope _tuitioScope; + private readonly IUserService _userService; + + public UserServiceTests(DependencyInjectionFixture fixture) + { + _tuitioScope = fixture.ServiceProvider.GetRequiredService().CreateScope(); + _userService = _tuitioScope.ServiceProvider.GetRequiredService(); + } + + public void Dispose() + { + _tuitioScope.Dispose(); + } + + [Fact] + public async Task Login_ShouldThrowArgumentExceptionIfUserNameIsNullOrEmptyString() + { + // Arrange + var userName = ""; + var password = ""; + + // Act + + // Assert + await Assert.ThrowsAsync(nameof(userName), () => _userService.Login(userName, password)); + await Assert.ThrowsAsync(nameof(userName), () => _userService.Login(null, password)); + } + + [Fact] + public async Task Login_ShouldThrowArgumentExceptionIfPasswordIsNullOrEmptyString() + { + // Arrange + var userName = "tuitio.test.user"; + var password = ""; + + // Act + + // Assert + await Assert.ThrowsAsync(nameof(password), () => _userService.Login(userName, password)); + await Assert.ThrowsAsync(nameof(password), () => _userService.Login(userName, null)); + } + + [Fact] + public async Task Login_ShouldReturnAValidToken() + { + // Arrange + var userName = "tuitio.user"; + var password = "pass123"; + + // Act + var result = await _userService.Login(userName, password); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Token); + Assert.NotEmpty(result.Raw); + Assert.Equal(userName, result.Token.UserName); + Assert.True(result.Token.TokenId != Guid.Empty, "Token id cannot be an empty guid."); + Assert.NotEmpty(result.Token.LockStamp); + Assert.True(result.Token.ExpiresIn > 0, "Token expiration must be a positive number."); + Assert.True((DateTime.UtcNow - result.Token.CreatedAt).TotalMinutes <= 1, "Token creation date must be within the last minute."); + } + + [Fact] + public async Task Login_ShouldSetCorrectLastLoginDate() + { + // Arrange + var userName = "tuitio.user"; + var password = "pass123"; + var dbContext = _tuitioScope.ServiceProvider.GetRequiredService(); + await _userService.Login(userName, password); + + // Act + var userFromDb = await dbContext.Users.FirstOrDefaultAsync(z => z.UserName == userName); + + // Assert + Assert.NotNull(userFromDb); + Assert.True(userFromDb.LastLoginDate.HasValue, "Last login date cannot be null after user login"); + Assert.True((DateTime.UtcNow - userFromDb.LastLoginDate.Value).TotalMinutes <= 1, "Last login date must be within the last minute."); + } + + [Fact] + public async Task Login_ShouldSaveTokenInDatabase() + { + // Arrange + var userName = "tuitio.user"; + var password = "pass123"; + var dbContext = _tuitioScope.ServiceProvider.GetRequiredService(); + var loginResult = await _userService.Login(userName, password); + + // Act + var userTokenFromDb = await dbContext.UserTokens.FirstOrDefaultAsync(z => z.TokenId == loginResult.Token.TokenId); + + // Assert + Assert.NotNull(userTokenFromDb); + Assert.True(loginResult.Token.TokenId != Guid.Empty, "Token id cannot be an empty guid."); + Assert.True((DateTime.UtcNow - userTokenFromDb.ValidFrom).TotalMinutes <= 1, "Token valid from date must be within the last minute."); + Assert.True(userTokenFromDb.ValidUntil > DateTime.UtcNow, "Token valid until date must be greater than the current date."); + } + + [Fact] + public async Task Login_ShouldReturnNullResultForWrongCredentials() + { + // Arrange + var userName = "tuitio.user"; + var password = "wrong_password"; + + // Act + var result = await _userService.Login(userName, password); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task Authorize_ShouldReturnAValidToken() + { + // Arrange + var userName = "tuitio.user"; + var password = "pass123"; + var loginResult = await _userService.Login(userName, password); + + // Act + var result = _userService.Authorize(loginResult.Raw); + + // Assert + Assert.NotNull(result); + Assert.Equal(userName, result.UserName); + Assert.NotNull(result.SecurityStamp); + Assert.NotNull(result.LockStamp); + Assert.True(result.TokenId != Guid.Empty, "Token id cannot be an empty guid."); + Assert.True(result.ExpiresIn > 0, "Token expiration must be a positive number."); + } + + [Fact] + public void Authorize_ShouldReturnNull() + { + // Arrange + var unauthorizedToken = "unauthorized-token"; + + // Act + var result = _userService.Authorize(unauthorizedToken); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task Logout_ShouldSuccessfullyLogoutTheUser() + { + // Arrange + var userName = "tuitio.user"; + var password = "pass123"; + var loginResult = await _userService.Login(userName, password); + + // Act + LogoutResult result; + using (var scope = _tuitioScope.ServiceProvider.CreateScope()) + { + var _newUserService = scope.ServiceProvider.GetRequiredService(); + result = await _newUserService.Logout(loginResult.Raw); + } + + // Assert + Assert.NotNull(result); + Assert.Equal(userName, result.UserName); + Assert.True((DateTime.UtcNow - result.LogoutDate).TotalMinutes <= 1, "Logout date must be within the last minute."); + } + + [Fact] + public async Task Logout_ShouldReturnNullResultForUnauthenticatedToken() + { + // Arrange + var unauthenticatedToken = "unauthenticated-token"; + + // Act + var result = await _userService.Logout(unauthenticatedToken); + + // Assert + Assert.Null(result); + } + } +}