using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using IamService.Infrastructure; using IamService.Infrastructure.Caching; using IamService.Domain.AggregatesModel.UserAggregate; using IamService.Domain.AggregatesModel.RoleAggregate; using Microsoft.AspNetCore.Identity; using StackExchange.Redis; namespace IamService.FunctionalTests; /// /// EN: Custom WebApplicationFactory for functional tests with Duende IdentityServer. /// VI: WebApplicationFactory tùy chỉnh cho functional tests với Duende IdentityServer. /// public class CustomWebApplicationFactory : WebApplicationFactory { private readonly string _databaseName = "TestDatabase_" + Guid.NewGuid().ToString(); protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); // EN: Configure services BEFORE the app configures itself // VI: Cấu hình services TRƯỚC KHI app tự cấu hình builder.ConfigureServices(services => { // EN: Remove ALL existing DbContext registrations // VI: Xóa TẤT CẢ các đăng ký DbContext hiện có RemoveExistingDbContextRegistrations(services); // EN: Remove Redis-related registrations // VI: Xóa các đăng ký liên quan đến Redis RemoveRedisRegistrations(services); // 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 services.AddDbContext(options => { options.UseInMemoryDatabase(_databaseName); options.EnableSensitiveDataLogging(); }); // EN: Set logging level for debugging tests // VI: Đặt mức logging để debug tests services.AddLogging(logging => { logging.SetMinimumLevel(LogLevel.Warning); logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning); logging.AddFilter("Duende.IdentityServer", LogLevel.Warning); }); }); // EN: Configure services AFTER the app is configured - this runs after Program.cs // VI: Cấu hình services SAU KHI app được cấu hình - chạy sau Program.cs builder.ConfigureTestServices(services => { // EN: Remove JWT Bearer and add custom test authentication handler // VI: Xóa JWT Bearer và thêm custom test authentication handler // EN: Override the authentication scheme options to use our test handler // VI: Override authentication scheme options để sử dụng test handler services.Configure(options => { // EN: Keep Bearer as default but use our handler // VI: Giữ Bearer là mặc định nhưng sử dụng handler của chúng ta }); services.PostConfigure(JwtBearerDefaults.AuthenticationScheme, options => { // EN: Configure minimal validation // VI: Cấu hình validation tối thiểu options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = false, ValidateIssuerSigningKey = false, RequireSignedTokens = false, // EN: ASP.NET Core 8+ requires JsonWebToken, not JwtSecurityToken // VI: ASP.NET Core 8+ yêu cầu JsonWebToken, không phải JwtSecurityToken SignatureValidator = (token, parameters) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token) }; // EN: Use static configuration // VI: Sử dụng static configuration options.RequireHttpsMetadata = false; var config = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration { Issuer = "http://localhost" }; options.ConfigurationManager = new Microsoft.IdentityModel.Protocols.StaticConfigurationManager(config); // EN: Event to help debug authentication issues // VI: Event để giúp debug authentication issues options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { var logger = context.HttpContext.RequestServices.GetService()?.CreateLogger("JwtBearerEvents"); logger?.LogError(context.Exception, "JWT authentication failed: {Message}", context.Exception.Message); return Task.CompletedTask; }, OnMessageReceived = context => { // EN: Ensure the token is extracted correctly // VI: Đảm bảo token được extract đúng var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { context.Token = authHeader.Substring("Bearer ".Length).Trim(); } return Task.CompletedTask; } }; }); }); } private static void RemoveExistingDbContextRegistrations(IServiceCollection services) { // 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(d => d.ServiceType == typeof(DbContextOptions) || d.ServiceType == typeof(IamServiceContext) || d.ServiceType == typeof(DbContextOptions) || d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true || d.ImplementationType?.FullName?.Contains("Npgsql") == true) .ToList(); foreach (var descriptor in descriptorsToRemove) { services.Remove(descriptor); } services.RemoveAll(typeof(DbContextOptions)); services.RemoveAll(typeof(DbContextOptions)); } private static void RemoveRedisRegistrations(IServiceCollection services) { // EN: Remove Redis-related registrations // VI: Xóa các đăng ký liên quan đến Redis 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: 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)); } }