Added unit tests
parent
3f0412e7c5
commit
25baaa0a67
|
@ -31,6 +31,8 @@ namespace Tuitio.Application.Services
|
||||||
|
|
||||||
public async Task<LoginResult> Login(string userName, string password)
|
public async Task<LoginResult> Login(string userName, string password)
|
||||||
{
|
{
|
||||||
|
ValidateCredentials(userName, password);
|
||||||
|
|
||||||
var passwordHash = _hashingService.HashSha256(password);
|
var passwordHash = _hashingService.HashSha256(password);
|
||||||
var user = await _userRepository.GetUser(userName, passwordHash);
|
var user = await _userRepository.GetUser(userName, passwordHash);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
|
@ -69,6 +71,15 @@ namespace Tuitio.Application.Services
|
||||||
return token;
|
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)
|
private bool ValidateUser(AppUser user)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
// Copyright (c) 2020 Tudor Stanciu
|
// 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.DbContexts;
|
||||||
using Tuitio.Domain.Data.Repositories;
|
using Tuitio.Domain.Data.Repositories;
|
||||||
using Tuitio.Domain.Repositories;
|
using Tuitio.Domain.Repositories;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Tuitio.Domain.Data
|
namespace Tuitio.Domain.Data
|
||||||
{
|
{
|
||||||
public static class DependencyInjectionExtensions
|
public static class DependencyInjectionExtensions
|
||||||
{
|
{
|
||||||
public static void AddDataAccess(this IServiceCollection services)
|
public static void AddDataAccessServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IUserRepository, UserRepository>();
|
services.AddScoped<IUserRepository, UserRepository>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AddDataAccess(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddDataAccessServices();
|
||||||
services
|
services
|
||||||
.AddDbContextPool<TuitioDbContext>(
|
.AddDbContextPool<TuitioDbContext>(
|
||||||
(serviceProvider, options) =>
|
(serviceProvider, options) =>
|
||||||
|
|
|
@ -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<DependencyInjectionFixture>, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IServiceScope _tuitioScope;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public CommandHandlerTests(DependencyInjectionFixture fixture)
|
||||||
|
{
|
||||||
|
_tuitioScope = fixture.ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||||
|
_mediator = _tuitioScope.ServiceProvider.GetRequiredService<IMediator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
using MediatR;
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using MediatR;
|
||||||
using MediatR.Pipeline;
|
using MediatR.Pipeline;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
@ -6,6 +8,7 @@ using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
|
using Tuitio.Domain.Data;
|
||||||
using Tuitio.Domain.Data.DbContexts;
|
using Tuitio.Domain.Data.DbContexts;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
@ -65,6 +68,7 @@ namespace Tuitio.Application.Tests.Fixtures
|
||||||
options.EnableDetailedErrors();
|
options.EnableDetailedErrors();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddDataAccessServices();
|
||||||
services.AddApplicationServices();
|
services.AddApplicationServices();
|
||||||
|
|
||||||
var provider = services.BuildServiceProvider();
|
var provider = services.BuildServiceProvider();
|
||||||
|
@ -92,6 +96,7 @@ namespace Tuitio.Application.Tests.Fixtures
|
||||||
{
|
{
|
||||||
using var dbContext = ServiceProvider.GetRequiredService<TuitioDbContext>();
|
using var dbContext = ServiceProvider.GetRequiredService<TuitioDbContext>();
|
||||||
|
|
||||||
|
#region UserStatus
|
||||||
dbContext.UserStatuses.AddRange(new Domain.Entities.UserStatus[]
|
dbContext.UserStatuses.AddRange(new Domain.Entities.UserStatus[]
|
||||||
{
|
{
|
||||||
new Domain.Entities.UserStatus()
|
new Domain.Entities.UserStatus()
|
||||||
|
@ -113,6 +118,23 @@ namespace Tuitio.Application.Tests.Fixtures
|
||||||
StatusName = "Blocked"
|
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();
|
dbContext.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
using Tuitio.Application.Services;
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Tuitio.Application.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Tuitio.Application.Tests
|
namespace Tuitio.Application.Tests
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Tuitio.Application.Tests.Fixtures;
|
using Tuitio.Application.Tests.Fixtures;
|
||||||
using Tuitio.Domain.Data.DbContexts;
|
using Tuitio.Domain.Data.DbContexts;
|
||||||
|
|
|
@ -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.Services.Abstractions;
|
||||||
using Tuitio.Application.Tests.Fixtures;
|
using Tuitio.Application.Tests.Fixtures;
|
||||||
using Tuitio.Domain.Entities;
|
using Tuitio.Domain.Entities;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
using Tuitio.Application.Stores;
|
// Copyright (c) 2020 Tudor Stanciu
|
||||||
|
|
||||||
|
using Tuitio.Application.Stores;
|
||||||
using Tuitio.Domain.Models;
|
using Tuitio.Domain.Models;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
|
@ -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<DependencyInjectionFixture>, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IServiceScope _tuitioScope;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
|
||||||
|
public UserServiceTests(DependencyInjectionFixture fixture)
|
||||||
|
{
|
||||||
|
_tuitioScope = fixture.ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||||
|
_userService = _tuitioScope.ServiceProvider.GetRequiredService<IUserService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_tuitioScope.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Login_ShouldThrowArgumentExceptionIfUserNameIsNullOrEmptyString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var userName = "";
|
||||||
|
var password = "";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(nameof(userName), () => _userService.Login(userName, password));
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(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<ArgumentException>(nameof(password), () => _userService.Login(userName, password));
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(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<TuitioDbContext>();
|
||||||
|
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<TuitioDbContext>();
|
||||||
|
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<IUserService>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue