Files
SIG-CM2.0/tests/SIGCM2.TestSupport/TestWebAppFactory.cs
dmolinari 3d598faffc feat(api): UDT-003 registro de usuarios — backend completo (Phases 1-6)
- Domain: Usuario.ForCreation factory, UsernameAlreadyExistsException, IUsuarioRepository extendido
- Application: CreateUsuarioCommand/Validator/Handler, UsuarioCreatedDto, AuthOptions password policy
- Infrastructure: UsuarioRepository.ExistsByUsernameAsync + AddAsync (INSERT OUTPUT INSERTED.Id), RoleClaimType="rol" en TokenValidationParameters
- Api: UsuariosController POST api/v1/users [Authorize(Roles="admin")], ExceptionFilter mapea UsernameAlreadyExistsException + SqlException 2627 → 409
- Tests (unit): 43 tests — 33 validator + 10 handler (107 total, green)
- Tests (integration): 7 tests CreateUsuarioEndpoint — 401/403/400/201/409/race/e2e (green)
- Fix: TestWebAppFactory.ConfigureTestServices reemplaza SqlConnectionFactory singleton con CS de test correcto
2026-04-15 10:47:48 -03:00

114 lines
4.7 KiB
C#

using System.Security.Cryptography;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SIGCM2.Application.Abstractions.Security;
using SIGCM2.Infrastructure.Persistence;
using SIGCM2.Infrastructure.Security;
using Xunit;
namespace SIGCM2.TestSupport;
/// <summary>
/// WebApplicationFactory for integration tests against SIGCM2.Api.
/// Uses SIGCM2_Test database (separate from production SIGCM2).
/// </summary>
public sealed class TestWebAppFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private const string TestConnectionString =
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
// Resolved once — absolute paths independent of working directory
private static readonly string RepoRoot = ResolveRepoRoot();
private static readonly string PrivateKeyPath = Path.Combine(RepoRoot, "src", "api", "SIGCM2.Api", "keys", "private.pem");
private static readonly string PublicKeyPath = Path.Combine(RepoRoot, "src", "api", "SIGCM2.Api", "keys", "public.pem");
private readonly SqlTestFixture _dbFixture = new(TestConnectionString);
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Step 1: Override configuration BEFORE services are built
builder.ConfigureAppConfiguration((ctx, config) =>
{
// Clear all existing sources and rebuild with test values
// This ensures our paths win over appsettings.json
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:SqlServer"] = TestConnectionString,
["Jwt:Issuer"] = "sigcm2.api",
["Jwt:Audience"] = "sigcm2.web",
["Jwt:AccessTokenMinutes"] = "60",
["Jwt:RefreshTokenDays"] = "7",
["Jwt:PrivateKeyPath"] = PrivateKeyPath,
["Jwt:PublicKeyPath"] = PublicKeyPath,
["Jwt:PrivateKey"] = null,
["Jwt:PublicKey"] = null,
["Cors:AllowedOrigins:0"] = "http://localhost:5173",
["Serilog:MinimumLevel:Default"] = "Warning",
});
});
builder.UseEnvironment("Testing");
// Step 2: Replace SqlConnectionFactory singleton with the correct test connection.
// ConfigureAppConfiguration alone is insufficient because WebApplication.CreateBuilder
// evaluates configuration for singleton construction before overrides apply.
// ConfigureTestServices runs AFTER all services are registered, so it wins.
builder.ConfigureTestServices(services =>
{
// Remove the existing SqlConnectionFactory singleton registered by AddInfrastructure
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(SqlConnectionFactory));
if (descriptor is not null)
services.Remove(descriptor);
// Re-register with the test connection string
services.AddSingleton(new SqlConnectionFactory(TestConnectionString));
});
}
public async Task InitializeAsync()
{
await _dbFixture.InitializeAsync();
}
public new async Task DisposeAsync()
{
await _dbFixture.DisposeAsync();
await base.DisposeAsync();
}
private static string ResolveRepoRoot()
{
// Walk up from AppContext.BaseDirectory looking for SIGCM2.slnx
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null)
{
if (dir.GetFiles("SIGCM2.slnx").Length > 0)
return dir.FullName;
dir = dir.Parent;
}
// Walk up from assembly location
var assemblyLocation = typeof(TestWebAppFactory).Assembly.Location;
dir = new DirectoryInfo(Path.GetDirectoryName(assemblyLocation)!);
while (dir is not null)
{
if (dir.GetFiles("SIGCM2.slnx").Length > 0)
return dir.FullName;
dir = dir.Parent;
}
// Known absolute path (last resort for this machine)
const string knownPath = @"E:\SIG-CM2.0";
if (Directory.Exists(knownPath) && File.Exists(Path.Combine(knownPath, "SIGCM2.slnx")))
return knownPath;
throw new InvalidOperationException(
$"Could not find repo root containing SIGCM2.slnx. " +
$"AppContext.BaseDirectory: {AppContext.BaseDirectory}");
}
}