diff --git a/src/Tuitio.Domain.Data/DbContexts/TuitioDbContext.cs b/src/Tuitio.Domain.Data/DbContexts/TuitioDbContext.cs index 0b5580d..497966b 100644 --- a/src/Tuitio.Domain.Data/DbContexts/TuitioDbContext.cs +++ b/src/Tuitio.Domain.Data/DbContexts/TuitioDbContext.cs @@ -8,6 +8,7 @@ namespace Tuitio.Domain.Data.DbContexts { public class TuitioDbContext : DbContext { + public DbSet UserStatuses{ get; set; } public DbSet Users { get; set; } public DbSet UserTokens { get; set; } diff --git a/src/Tuitio/Extensions/StartupExtensions.cs b/src/Tuitio/Extensions/StartupExtensions.cs index 9d14d30..03e9e76 100644 --- a/src/Tuitio/Extensions/StartupExtensions.cs +++ b/src/Tuitio/Extensions/StartupExtensions.cs @@ -27,8 +27,7 @@ namespace Tuitio.Extensions services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>)); // AutoMapper - services.AddAutoMapper( - typeof(Application.Mappings.MappingProfile).Assembly); + services.AddAutoMapper(typeof(Application.Mappings.MappingProfile).Assembly); // Swagger services.AddSwagger("Tuitio API", AuthorizationType.None); diff --git a/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs b/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs new file mode 100644 index 0000000..1941a57 --- /dev/null +++ b/test/UnitTests/Tuitio.Application.Tests/Fixtures/DependencyInjectionFixture.cs @@ -0,0 +1,120 @@ +using MediatR; +using MediatR.Pipeline; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Data.Common; +using Tuitio.Domain.Data.DbContexts; +using Xunit; + +namespace Tuitio.Application.Tests.Fixtures +{ + public class DependencyInjectionFixture : IAsyncLifetime + { + private readonly Guid _prefix; + private readonly List _connections; + public readonly IServiceProvider ServiceProvider; + + public DependencyInjectionFixture() + { + _prefix = Guid.NewGuid(); + _connections = new List(); + ServiceProvider = BuildServiceProvider(); + } + + public async Task DisposeAsync() + { + foreach (var con in _connections) + await con.DisposeAsync(); + } + + public async Task InitializeAsync() + { + await PrepareDbAsync(); + SeedData(); + } + + private IServiceProvider BuildServiceProvider() + { + var configurationBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.tuitio.json", optional: true, reloadOnChange: false); + var configuration = configurationBuilder.Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + services.AddLogging(builder => + { + builder.SetMinimumLevel(LogLevel.Warning); + builder.AddFilter("Microsoft.*", LogLevel.Warning); + }); + + // MediatR + services.AddMediatR(typeof(Application.CommandHandlers.AccountLoginHandler).Assembly); + services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>)); + + // AutoMapper + services.AddAutoMapper(typeof(Application.Mappings.MappingProfile).Assembly); + + services.AddDbContext(options => + { + options.UseSqlite(GetOrAddInMemoryDatabase("TuitioDbContext")); + options.EnableSensitiveDataLogging(); + options.EnableDetailedErrors(); + }); + + services.AddApplicationServices(); + + var provider = services.BuildServiceProvider(); + return provider; + } + + private DbConnection GetOrAddInMemoryDatabase(string code) + { + var connection = new SqliteConnection($"Data Source={_prefix}{code};Mode=Memory;Cache=shared"); + _connections.Add(connection); + connection.Open(); + return connection; + } + + private async Task PrepareDbAsync() + { + var result = await ServiceProvider.GetRequiredService().Database.EnsureCreatedAsync(); + if (!result) throw new Exception("TuitioDbContext exists"); + + var script = await File.ReadAllTextAsync("seed.sql"); + await ServiceProvider.GetRequiredService().Database.ExecuteSqlRawAsync(script); + } + + private void SeedData() + { + using var dbContext = ServiceProvider.GetRequiredService(); + + dbContext.UserStatuses.AddRange(new Domain.Entities.UserStatus[] + { + new Domain.Entities.UserStatus() + { + StatusId = 1, + StatusCode = "ACTIVE", + StatusName = "Active" + }, + new Domain.Entities.UserStatus() + { + StatusId = 2, + StatusCode = "INACTIVE", + StatusName = "Inactive" + }, + new Domain.Entities.UserStatus() + { + StatusId = 3, + StatusCode = "BLOCKED", + StatusName = "Blocked" + } + }); + + dbContext.SaveChanges(); + } + } +} diff --git a/test/UnitTests/Tuitio.Application.Tests/HashingServiceTests.cs b/test/UnitTests/Tuitio.Application.Tests/HashingServiceTests.cs new file mode 100644 index 0000000..055e286 --- /dev/null +++ b/test/UnitTests/Tuitio.Application.Tests/HashingServiceTests.cs @@ -0,0 +1,22 @@ +using Tuitio.Application.Services; +using Xunit; + +namespace Tuitio.Application.Tests +{ + public class HashingServiceTests + { + [Fact] + public void HashSha256_ShouldReturnCorrectHash() + { + // Arrange + var expected = "9B8769A4A742959A2D0298C36FB70623F2DFACDA8436237DF08D8DFD5B37374C"; + var hashingService = new HashingService(); + + // Act + var actual = hashingService.HashSha256("pass123"); + + // Assert + Assert.Equal(expected, actual); + } + } +} diff --git a/test/UnitTests/Tuitio.Application.Tests/LocalSqliteDatabaseTests.cs b/test/UnitTests/Tuitio.Application.Tests/LocalSqliteDatabaseTests.cs new file mode 100644 index 0000000..99722ed --- /dev/null +++ b/test/UnitTests/Tuitio.Application.Tests/LocalSqliteDatabaseTests.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Tuitio.Application.Tests.Fixtures; +using Tuitio.Domain.Data.DbContexts; +using Xunit; + +namespace Tuitio.Application.Tests +{ + public class LocalSqliteDatabaseTests : IClassFixture, IDisposable + { + private readonly IServiceScope _tuitioScope; + + public LocalSqliteDatabaseTests(DependencyInjectionFixture fixture) + { + _tuitioScope = fixture.ServiceProvider.GetRequiredService().CreateScope(); + } + + public void Dispose() + { + _tuitioScope.Dispose(); + } + + [Fact] + public async Task EnsureExpectedDataInLocalDbTest() + { + // Arrange + var _dbContext = _tuitioScope.ServiceProvider.GetRequiredService(); + + // Act + var statuses = await _dbContext.UserStatuses.ToArrayAsync(); + + // Assert + Assert.NotEmpty(statuses); + Assert.True(statuses.All(z => !string.IsNullOrWhiteSpace(z.StatusCode))); + Assert.Contains(statuses, z => z.StatusCode == "ACTIVE"); + } + } +} diff --git a/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs b/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs new file mode 100644 index 0000000..e60cb60 --- /dev/null +++ b/test/UnitTests/Tuitio.Application.Tests/TokenServiceTests.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.DependencyInjection; +using Tuitio.Application.Services.Abstractions; +using Tuitio.Application.Tests.Fixtures; +using Tuitio.Domain.Entities; +using Xunit; + +namespace Tuitio.Application.Tests +{ + public class TokenServiceTests : IClassFixture, IDisposable + { + private readonly IServiceScope _tuitioScope; + private readonly AppUser _userMock; + + public TokenServiceTests(DependencyInjectionFixture fixture) + { + _tuitioScope = fixture.ServiceProvider.GetRequiredService().CreateScope(); + _userMock = MockAppUser(); + } + + private AppUser MockAppUser() + { + var user = new AppUser() + { + UserId = 0, + UserName = "tuitio.test", + Password = "9B8769A4A742959A2D0298C36FB70623F2DFACDA8436237DF08D8DFD5B37374C", //pass123 + Email = "tuitio.test@test.test", + FirstName = "tuitio", + LastName = "test", + StatusId = 1, + FailedLoginAttempts = 0, + SecurityStamp = "A93650FF-1FC4-4999-BAB6-3EEB174F6892", + CreationDate = DateTime.Now + }; + + return user; + } + + public void Dispose() + { + _tuitioScope.Dispose(); + } + + [Fact] + public void TokenGenerationTest() + { + // Arrange + var _tokenService = _tuitioScope.ServiceProvider.GetRequiredService(); + + // Act + var token = _tokenService.GenerateToken(_userMock); + var raw = _tokenService.GenerateTokenRaw(token); + var extracted = _tokenService.ExtractToken(raw); + + // Assert + Assert.NotNull(token); + Assert.NotNull(raw); + Assert.NotNull(extracted); + + Assert.True(_userMock.UserName == extracted.UserName); + Assert.True(_userMock.SecurityStamp == extracted.SecurityStamp); + } + } +} diff --git a/test/UnitTests/Tuitio.Application.Tests/Tuitio.Application.Tests.csproj b/test/UnitTests/Tuitio.Application.Tests/Tuitio.Application.Tests.csproj index 427a0d7..b89da9f 100644 --- a/test/UnitTests/Tuitio.Application.Tests/Tuitio.Application.Tests.csproj +++ b/test/UnitTests/Tuitio.Application.Tests/Tuitio.Application.Tests.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -7,7 +7,23 @@ + + + + + + PreserveNewest + + + + + + + + + + all @@ -17,6 +33,13 @@ + + + + + + PreserveNewest + diff --git a/test/UnitTests/Tuitio.Application.Tests/appsettings.tuitio.json b/test/UnitTests/Tuitio.Application.Tests/appsettings.tuitio.json new file mode 100644 index 0000000..de62a43 --- /dev/null +++ b/test/UnitTests/Tuitio.Application.Tests/appsettings.tuitio.json @@ -0,0 +1,8 @@ +{ + "Restrictions": { + "MaxFailedLoginAttempts": 5 + }, + "Token": { + "ValidityInMinutes": 43800 + } +} \ No newline at end of file diff --git a/test/UnitTests/Tuitio.Application.Tests/seed.sql b/test/UnitTests/Tuitio.Application.Tests/seed.sql new file mode 100644 index 0000000..76b88b2 --- /dev/null +++ b/test/UnitTests/Tuitio.Application.Tests/seed.sql @@ -0,0 +1,3 @@ + +SELECT 'empty' +