diff --git a/services/iam-service-net/src/IamService.API/Application/Behaviors/TransactionBehavior.cs b/services/iam-service-net/src/IamService.API/Application/Behaviors/TransactionBehavior.cs index c041d0e0..cc686624 100644 --- a/services/iam-service-net/src/IamService.API/Application/Behaviors/TransactionBehavior.cs +++ b/services/iam-service-net/src/IamService.API/Application/Behaviors/TransactionBehavior.cs @@ -1,5 +1,6 @@ using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using IamService.Infrastructure; namespace IamService.API.Application.Behaviors; @@ -44,6 +45,13 @@ public class TransactionBehavior : IPipelineBehavior diff --git a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs index bf32ed4c..905b1fa9 100644 --- a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs +++ b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs @@ -21,7 +21,8 @@ public static class DependencyInjection /// public static IServiceCollection AddInfrastructure( this IServiceCollection services, - IConfiguration configuration) + IConfiguration configuration, + string? environmentName = null) { // EN: Get database connection string // VI: Lấy database connection string @@ -140,19 +141,23 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService()); - // EN: Configure Redis caching - // VI: Cấu hình Redis caching - var redisSettings = new Caching.RedisSettings(); - configuration.GetSection(Caching.RedisSettings.SectionName).Bind(redisSettings); - - services.AddSingleton(sp => + // EN: Configure Redis caching (skip in Testing environment) + // VI: Cấu hình Redis caching (bỏ qua trong Testing environment) + if (!string.Equals(environmentName, "Testing", StringComparison.OrdinalIgnoreCase)) { - var connectionString = redisSettings.GetConnectionString(); - return StackExchange.Redis.ConnectionMultiplexer.Connect(connectionString); - }); - - services.AddSingleton(); + var redisSettings = new Caching.RedisSettings(); + configuration.GetSection(Caching.RedisSettings.SectionName).Bind(redisSettings); + + services.AddSingleton(sp => + { + var connectionString = redisSettings.GetConnectionString(); + return StackExchange.Redis.ConnectionMultiplexer.Connect(connectionString); + }); + + services.AddSingleton(); + } return services; } } + diff --git a/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/AuthControllerTests.cs b/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/AuthControllerTests.cs index 1fbd2ed2..94c6914f 100644 --- a/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/AuthControllerTests.cs +++ b/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/AuthControllerTests.cs @@ -41,6 +41,13 @@ public class AuthControllerTests : IClassFixture // Act var response = await _client.PostAsJsonAsync("/api/v1/auth/register", request); + // Debug: Print response body if not Created + if (response.StatusCode != HttpStatusCode.Created) + { + var body = await response.Content.ReadAsStringAsync(); + throw new Exception($"Expected Created, got {response.StatusCode}. Body: {body}"); + } + // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); diff --git a/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs b/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs index 194eb1d3..67cd954f 100644 --- a/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs +++ b/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs @@ -4,6 +4,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using IamService.Infrastructure; +using IamService.Infrastructure.Caching; +using StackExchange.Redis; namespace IamService.FunctionalTests; @@ -19,6 +21,20 @@ public class CustomWebApplicationFactory : WebApplicationFactory builder.ConfigureServices(services => { + // EN: Remove Redis-related registrations FIRST (before they get resolved) + // VI: Xóa các đăng ký liên quan đến Redis TRƯỚC TIÊN (trước khi chúng được resolve) + var redisDescriptors = services.Where(d => + d.ServiceType == typeof(IConnectionMultiplexer) || + d.ServiceType == typeof(ICacheService) || + d.ImplementationType?.FullName?.Contains("Redis") == true || + d.ServiceType.FullName?.Contains("Redis") == true) + .ToList(); + + foreach (var descriptor in redisDescriptors) + { + services.Remove(descriptor); + } + // EN: Remove ALL DbContext-related registrations // VI: Xóa TẤT CẢ các đăng ký liên quan đến DbContext var descriptorsToRemove = services.Where( @@ -37,20 +53,134 @@ public class CustomWebApplicationFactory : WebApplicationFactory // VI: Xóa DbContextOptions generic services.RemoveAll(typeof(DbContextOptions)); + // EN: Add mock cache service for testing + // VI: Thêm mock cache service để test + services.AddSingleton(); + // EN: Add in-memory database for testing // VI: Thêm in-memory database để test + var dbName = "TestDatabase_" + Guid.NewGuid().ToString(); services.AddDbContext(options => { - options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString()); + options.UseInMemoryDatabase(dbName); + options.UseOpenIddict(); // EN: Required for OpenIddict / VI: Cần cho OpenIddict options.EnableSensitiveDataLogging(); }); - - // EN: Ensure database is created with seed data - // VI: Đảm bảo database được tạo với seed data - var sp = services.BuildServiceProvider(); - using var scope = sp.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); }); } + + /// + /// EN: Ensure database is created after host is built + /// VI: Đảm bảo database được tạo sau khi host được build + /// + protected override void ConfigureClient(HttpClient client) + { + base.ConfigureClient(client); + + // EN: Create the database when the first client is created + // VI: Tạo database khi client đầu tiên được tạo + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); + } +} + +/// +/// EN: In-memory cache service for testing +/// VI: Cache service in-memory cho testing +/// +public class InMemoryCacheService : ICacheService +{ + private readonly Dictionary _cache = new(); + private readonly HashSet _blacklist = new(); + + public Task GetAsync(string key, CancellationToken cancellationToken = default) where T : class + { + if (_cache.TryGetValue(key, out var entry)) + { + if (entry.Expiry == null || entry.Expiry > DateTime.UtcNow) + { + return Task.FromResult((T?)entry.Value); + } + _cache.Remove(key); + } + return Task.FromResult(null); + } + + public Task GetStringAsync(string key, CancellationToken cancellationToken = default) + { + if (_cache.TryGetValue(key, out var entry)) + { + if (entry.Expiry == null || entry.Expiry > DateTime.UtcNow) + { + return Task.FromResult((string?)entry.Value); + } + _cache.Remove(key); + } + return Task.FromResult(null); + } + + public Task SetAsync(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default) where T : class + { + var expiry = expiration.HasValue ? DateTime.UtcNow.Add(expiration.Value) : (DateTime?)null; + _cache[key] = (value, expiry); + return Task.CompletedTask; + } + + public Task SetStringAsync(string key, string value, TimeSpan? expiration = null, CancellationToken cancellationToken = default) + { + var expiry = expiration.HasValue ? DateTime.UtcNow.Add(expiration.Value) : (DateTime?)null; + _cache[key] = (value, expiry); + return Task.CompletedTask; + } + + public Task RemoveAsync(string key, CancellationToken cancellationToken = default) + { + _cache.Remove(key); + return Task.CompletedTask; + } + + public Task RemoveByPatternAsync(string pattern, CancellationToken cancellationToken = default) + { + var keysToRemove = _cache.Keys.Where(k => k.Contains(pattern.Replace("*", ""))).ToList(); + foreach (var key in keysToRemove) + { + _cache.Remove(key); + } + return Task.CompletedTask; + } + + public Task ExistsAsync(string key, CancellationToken cancellationToken = default) + { + if (_cache.TryGetValue(key, out var entry)) + { + if (entry.Expiry == null || entry.Expiry > DateTime.UtcNow) + { + return Task.FromResult(true); + } + _cache.Remove(key); + } + return Task.FromResult(false); + } + + public async Task GetOrSetAsync(string key, Func> factory, TimeSpan? expiration = null, CancellationToken cancellationToken = default) where T : class + { + var cached = await GetAsync(key, cancellationToken); + if (cached != null) return cached; + + var value = await factory(); + await SetAsync(key, value, expiration, cancellationToken); + return value; + } + + public Task BlacklistAsync(string key, TimeSpan expiration, CancellationToken cancellationToken = default) + { + _blacklist.Add(key); + return Task.CompletedTask; + } + + public Task IsBlacklistedAsync(string key, CancellationToken cancellationToken = default) + { + return Task.FromResult(_blacklist.Contains(key)); + } } diff --git a/services/iam-service-net/tests/IamService.UnitTests/Application/Commands/ChangePasswordCommandHandlerTests.cs b/services/iam-service-net/tests/IamService.UnitTests/Application/Commands/ChangePasswordCommandHandlerTests.cs index d81796af..57943183 100644 --- a/services/iam-service-net/tests/IamService.UnitTests/Application/Commands/ChangePasswordCommandHandlerTests.cs +++ b/services/iam-service-net/tests/IamService.UnitTests/Application/Commands/ChangePasswordCommandHandlerTests.cs @@ -67,7 +67,7 @@ public class ChangePasswordCommandHandlerTests } [Fact] - public async Task Handle_WithWrongCurrentPassword_ShouldThrowException() + public async Task Handle_WithWrongCurrentPassword_ShouldReturnFailure() { // Arrange var userId = Guid.NewGuid(); @@ -83,8 +83,11 @@ public class ChangePasswordCommandHandlerTests .ReturnsAsync(IdentityResult.Failed( new IdentityError { Code = "PasswordMismatch", Description = "Incorrect password" })); - // Act & Assert - await Assert.ThrowsAsync(() => - _handler.Handle(command, CancellationToken.None)); + // Act + var result = await _handler.Handle(command, CancellationToken.None); + + // Assert + Assert.False(result.Success); + Assert.Contains("Incorrect password", result.Message); } }