WebApiTestFactory.cs
3,856 bytes
| 1 | using App.DAL.EF; |
|---|---|
| 2 | using Microsoft.AspNetCore.Hosting; |
| 3 | using Microsoft.AspNetCore.Mvc.Testing; |
| 4 | using Microsoft.Data.Sqlite; |
| 5 | using Microsoft.EntityFrameworkCore; |
| 6 | using Microsoft.Extensions.Configuration; |
| 7 | using Microsoft.AspNetCore.Identity; |
| 8 | using Microsoft.Extensions.DependencyInjection; |
| 9 | using Microsoft.Extensions.Hosting; |
| 10 | |
| 11 | namespace App.Tests.Integration; |
| 12 | |
| 13 | /// <summary> |
| 14 | /// Real ASP.NET pipeline (controllers + auth + EF) but Postgres → SQLite in-memory. |
| 15 | /// One factory per test class via IClassFixture — DB lives for the class lifetime. |
| 16 | /// </summary> |
| 17 | public class WebApiTestFactory : WebApplicationFactory<Program> |
| 18 | { |
| 19 | private readonly SqliteConnection _connection; |
| 20 | |
| 21 | public WebApiTestFactory() |
| 22 | { |
| 23 | _connection = new SqliteConnection("Data Source=:memory:"); |
| 24 | _connection.Open(); |
| 25 | } |
| 26 | |
| 27 | protected override void ConfigureWebHost(IWebHostBuilder builder) |
| 28 | { |
| 29 | builder.UseEnvironment("Testing"); |
| 30 | |
| 31 | builder.ConfigureAppConfiguration((_, configBuilder) => |
| 32 | { |
| 33 | configBuilder.AddInMemoryCollection(new Dictionary<string, string?> |
| 34 | { |
| 35 | // Skip data initialization (handled by Program.cs env check) |
| 36 | ["DataInitialization:DropDatabase"] = "false", |
| 37 | ["DataInitialization:MigrateDatabase"] = "false", |
| 38 | ["DataInitialization:SeedIdentity"] = "false", |
| 39 | ["DataInitialization:SeedData"] = "false", |
| 40 | |
| 41 | // JWT (HS256 needs ≥32-byte key) |
| 42 | ["JWT:Key"] = "test-jwt-key-for-integration-tests-only-1234567890", |
| 43 | ["JWT:Issuer"] = "test-issuer", |
| 44 | ["JWT:Audience"] = "test-audience", |
| 45 | |
| 46 | ["SupportedCultures:0"] = "en", |
| 47 | ["SupportedCultures:1"] = "et", |
| 48 | |
| 49 | ["ConnectionStrings:DefaultConnection"] = "Host=replaced;Database=replaced", |
| 50 | }); |
| 51 | }); |
| 52 | |
| 53 | builder.ConfigureServices(services => |
| 54 | { |
| 55 | // Nuke EVERY EF-related descriptor that the real app registered (AddDbContext |
| 56 | // with UseNpgsql leaves behind ~15 DbContextOptions / EF service entries). |
| 57 | // Without this, both Npgsql AND Sqlite providers end up configured on the |
| 58 | // same DbContextOptions and EF throws "multiple providers" errors. |
| 59 | var efDescriptors = services.Where(d => |
| 60 | d.ServiceType.FullName != null && |
| 61 | (d.ServiceType.FullName.Contains("EntityFrameworkCore") || |
| 62 | d.ServiceType == typeof(AppDbContext)) |
| 63 | ).ToList(); |
| 64 | foreach (var d in efDescriptors) services.Remove(d); |
| 65 | |
| 66 | // Re-add fresh with SQLite only |
| 67 | services.AddDbContext<AppDbContext>(opts => opts.UseSqlite(_connection)); |
| 68 | }); |
| 69 | } |
| 70 | |
| 71 | /// <summary> |
| 72 | /// EnsureCreated runs against the fully-built service provider (host.Services), |
| 73 | /// not the half-built collection inside ConfigureServices. Also seeds the "user" |
| 74 | /// role since IdentityService.RegisterAsync calls AddToRoleAsync("user"). |
| 75 | /// </summary> |
| 76 | protected override IHost CreateHost(IHostBuilder builder) |
| 77 | { |
| 78 | var host = base.CreateHost(builder); |
| 79 | using var scope = host.Services.CreateScope(); |
| 80 | var sp = scope.ServiceProvider; |
| 81 | |
| 82 | var db = sp.GetRequiredService<AppDbContext>(); |
| 83 | db.Database.EnsureCreated(); |
| 84 | |
| 85 | var roleManager = sp.GetRequiredService<RoleManager<App.Domain.Identity.AppRole>>(); |
| 86 | if (!roleManager.RoleExistsAsync("user").GetAwaiter().GetResult()) |
| 87 | { |
| 88 | roleManager.CreateAsync(new App.Domain.Identity.AppRole { Name = "user" }) |
| 89 | .GetAwaiter().GetResult(); |
| 90 | } |
| 91 | |
| 92 | return host; |
| 93 | } |
| 94 | |
| 95 | protected override void Dispose(bool disposing) |
| 96 | { |
| 97 | base.Dispose(disposing); |
| 98 | if (disposing) _connection.Dispose(); |
| 99 | } |
| 100 | } |
| 101 | |