Files
pos-system/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs
Ho Ngoc Hai eb5cb28d9f feat(exceptions): Introduce custom exceptions for better error handling and validation
- Added custom exceptions: DuplicateResourceException, EntityNotFoundException, AuthenticationFailedException, and BusinessRuleException to improve error handling in the application.
- Updated Program.cs to map these exceptions to appropriate HTTP status codes and problem details for better client feedback.
- Refactored RegisterUserCommandHandler to throw DuplicateResourceException when a user with the same email already exists.
- Enhanced testing setup in CustomWebApplicationFactory to ensure proper handling of these exceptions during functional tests.
2026-01-12 20:04:38 +07:00

242 lines
9.0 KiB
C#

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using IamService.Infrastructure;
using IamService.Infrastructure.Caching;
using StackExchange.Redis;
namespace IamService.FunctionalTests;
/// <summary>
/// EN: Custom WebApplicationFactory for functional tests.
/// VI: WebApplicationFactory tùy chỉnh cho functional tests.
/// </summary>
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
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: Remove OpenIddict EF Core stores to re-register with in-memory db
// VI: Xóa OpenIddict EF Core stores để đăng ký lại với in-memory db
RemoveOpenIddictStores(services);
// EN: Add mock cache service for testing
// VI: Thêm mock cache service để test
services.AddSingleton<ICacheService, InMemoryCacheService>();
// EN: Add in-memory database for testing
// VI: Thêm in-memory database để test
services.AddDbContext<IamServiceContext>(options =>
{
options.UseInMemoryDatabase(_databaseName);
options.UseOpenIddict();
options.EnableSensitiveDataLogging();
});
// EN: Re-register OpenIddict Core with the new DbContext
// VI: Đăng ký lại OpenIddict Core với DbContext mới
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<IamServiceContext>();
});
// 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("OpenIddict", LogLevel.Warning);
});
});
}
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<IamServiceContext>) ||
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<IamServiceContext>));
}
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);
}
}
private static void RemoveOpenIddictStores(IServiceCollection services)
{
// EN: Remove OpenIddict EF Core store registrations to re-register with in-memory db
// VI: Xóa các đăng ký OpenIddict EF Core stores để đăng ký lại với in-memory db
var openIddictDescriptors = services.Where(d =>
d.ServiceType.FullName?.Contains("OpenIddict.EntityFrameworkCore") == true ||
d.ImplementationType?.FullName?.Contains("OpenIddict.EntityFrameworkCore") == true)
.ToList();
foreach (var descriptor in openIddictDescriptors)
{
services.Remove(descriptor);
}
}
/// <summary>
/// EN: Ensure database is created after host is built
/// VI: Đảm bảo database được tạo sau khi host được build
/// </summary>
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<IamServiceContext>();
db.Database.EnsureCreated();
}
}
/// <summary>
/// EN: In-memory cache service for testing
/// VI: Cache service in-memory cho testing
/// </summary>
public class InMemoryCacheService : ICacheService
{
private readonly Dictionary<string, (object Value, DateTime? Expiry)> _cache = new();
private readonly HashSet<string> _blacklist = new();
public Task<T?> GetAsync<T>(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<T?>(null);
}
public Task<string?> 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<string?>(null);
}
public Task SetAsync<T>(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<bool> 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<T?> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null, CancellationToken cancellationToken = default) where T : class
{
var cached = await GetAsync<T>(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<bool> IsBlacklistedAsync(string key, CancellationToken cancellationToken = default)
{
return Task.FromResult(_blacklist.Contains(key));
}
}