using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; 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.Extensions.Options; using MembershipService.Domain.AggregatesModel.ExperienceAggregate; using MembershipService.Domain.AggregatesModel.LevelAggregate; using MembershipService.Domain.AggregatesModel.MemberAggregate; using MembershipService.Infrastructure; using MembershipService.Infrastructure.Repositories; namespace MembershipService.FunctionalTests; /// /// EN: Custom WebApplicationFactory for functional tests with mock auth. /// VI: WebApplicationFactory tùy chỉnh cho functional tests với mock auth. /// public class CustomWebApplicationFactory : WebApplicationFactory { private bool _seeded; protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); builder.ConfigureServices(services => { // EN: Remove ALL DbContext and EF registrations // VI: Xóa TẤT CẢ các đăng ký DbContext và EF var descriptorsToRemove = services.Where( d => d.ServiceType == typeof(DbContextOptions) || d.ServiceType == typeof(MembershipServiceContext) || d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true || d.ServiceType.FullName?.Contains("Npgsql") == true || d.ServiceType.FullName?.Contains("DbContext") == true) .ToList(); foreach (var descriptor in descriptorsToRemove) { services.Remove(descriptor); } // EN: Add in-memory database for testing with unique name per factory // VI: Thêm in-memory database để test với tên duy nhất cho mỗi factory var databaseName = $"TestDb_{Guid.NewGuid()}"; services.AddDbContext(options => { options.UseInMemoryDatabase(databaseName); // EN: Suppress transaction warning since in-memory doesn't support transactions // VI: Bỏ qua cảnh báo transaction vì in-memory không hỗ trợ transactions options.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)); }); // EN: Re-register repositories // VI: Đăng ký lại repositories services.AddScoped(); services.AddScoped(); services.AddScoped(); }); builder.ConfigureTestServices(services => { // EN: Override authentication with test scheme // VI: Override authentication với test scheme services.AddAuthentication(options => { options.DefaultAuthenticateScheme = "Test"; options.DefaultChallengeScheme = "Test"; options.DefaultScheme = "Test"; }) .AddScheme("Test", options => { }); }); } /// /// EN: Create client with authenticated user. /// VI: Tạo client với user đã xác thực. /// public HttpClient CreateAuthenticatedClient(Guid? userId = null) { var client = CreateClient(); client.DefaultRequestHeaders.Add("X-Test-User-Id", (userId ?? Guid.NewGuid()).ToString()); return client; } /// /// EN: Seed level definitions into database. /// VI: Seed level definitions vào database. /// public async Task SeedLevelDefinitionsAsync() { if (_seeded) return; using var scope = Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); if (!await db.LevelDefinitions.AnyAsync()) { db.LevelDefinitions.AddRange( new LevelDefinition(1, "Bronze", 0, "Starting level", null, "#CD7F32"), new LevelDefinition(2, "Silver", 100, "Reach 100 EXP", null, "#C0C0C0"), new LevelDefinition(3, "Gold", 300, "Reach 300 EXP", null, "#FFD700"), new LevelDefinition(4, "Platinum", 600, "Reach 600 EXP", null, "#E5E4E2"), new LevelDefinition(5, "Diamond", 1000, "Reach 1000 EXP", null, "#B9F2FF") ); await db.SaveChangesAsync(); } _seeded = true; } } /// /// EN: Test authentication handler for functional tests. /// VI: Test authentication handler cho functional tests. /// public class TestAuthHandler : AuthenticationHandler { public TestAuthHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { } protected override Task HandleAuthenticateAsync() { // EN: Check for test user ID header // VI: Kiểm tra header test user ID if (!Request.Headers.TryGetValue("X-Test-User-Id", out var userIdValues)) { return Task.FromResult(AuthenticateResult.Fail("Missing X-Test-User-Id header")); } var userId = userIdValues.FirstOrDefault() ?? Guid.NewGuid().ToString(); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim("sub", userId), new Claim("id", userId), new Claim(ClaimTypes.Name, "Test User"), new Claim(ClaimTypes.Email, "test@example.com"), new Claim(ClaimTypes.Role, "Admin") }; var identity = new ClaimsIdentity(claims, "Test"); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, "Test"); return Task.FromResult(AuthenticateResult.Success(ticket)); } }