|
1 | 1 | using Microsoft.AspNetCore.Hosting; |
2 | 2 | using Microsoft.AspNetCore.Mvc.Testing; |
3 | | -using Microsoft.EntityFrameworkCore; |
4 | | -using Microsoft.Extensions.Configuration; |
5 | 3 | using Microsoft.Extensions.DependencyInjection; |
6 | 4 | using NSubstitute; |
7 | 5 | using SEBT.Portal.Core.Services; |
8 | | -using SEBT.Portal.Infrastructure.Data; |
9 | 6 | using SEBT.Portal.Infrastructure.Services; |
10 | | -using SEBT.Portal.StatesPlugins.Interfaces; |
11 | 7 |
|
12 | 8 | namespace SEBT.Portal.Tests.Integration; |
13 | 9 |
|
14 | 10 | /// <summary> |
15 | 11 | /// Shared test factory for integration tests that spin up the real HTTP pipeline. |
16 | 12 | /// Handles common concerns so individual test classes can focus on endpoint behavior: |
17 | 13 | /// <list type="bullet"> |
18 | | -/// <item>Configures plugin assembly paths to a non-existent directory so no plugins load</item> |
19 | | -/// <item>Replaces SQL Server with InMemory EF provider</item> |
20 | | -/// <item>Replaces database migration/seeding with no-op mocks</item> |
21 | | -/// <item>Mocks plugin service interfaces so tests don't depend on state plugins</item> |
| 14 | +/// <item>Redirects plugin assembly paths to prevent loading DLLs with missing transitive dependencies</item> |
| 15 | +/// <item>Replaces database services with no-op mocks (no SQL Server required)</item> |
22 | 16 | /// </list> |
23 | 17 | /// </summary> |
24 | 18 | public class PortalWebApplicationFactory : WebApplicationFactory<Program> |
25 | 19 | { |
26 | 20 | protected override void ConfigureWebHost(IWebHostBuilder builder) |
27 | 21 | { |
28 | | - builder.UseEnvironment("Development"); |
| 22 | + // Override plugin assembly paths via environment variables BEFORE the server starts. |
| 23 | + // WebApplicationFactory lazily starts the server, so env vars set here are visible |
| 24 | + // when Program.cs reads builder.Configuration during startup. |
| 25 | + // This prevents loading plugin DLLs (copied to test output by the API csproj) |
| 26 | + // that have unresolvable transitive dependencies in the test environment. |
| 27 | + Environment.SetEnvironmentVariable("PluginAssemblyPaths__0", "plugins-none"); |
| 28 | + Environment.SetEnvironmentVariable("PluginAssemblyPaths__1", "plugins-none"); |
29 | 29 |
|
30 | | - builder.ConfigureAppConfiguration((_, config) => |
31 | | - config.AddInMemoryCollection(new Dictionary<string, string?> |
32 | | - { |
33 | | - ["PluginAssemblyPaths:0"] = "plugins-test", |
34 | | - ["JwtSettings:SecretKey"] = |
35 | | - "integration-test-key-must-be-at-least-32-bytes-long", |
36 | | - })); |
| 30 | + // Provide a dummy JWT secret so the JwtBearer handler can initialize. |
| 31 | + // The auth middleware runs on every request (including /health), and |
| 32 | + // PostConfigure reads JwtSettings:SecretKey to create a SymmetricSecurityKey. |
| 33 | + Environment.SetEnvironmentVariable("JwtSettings__SecretKey", |
| 34 | + "integration-test-secret-key-at-least-32-chars!"); |
37 | 35 |
|
38 | 36 | builder.ConfigureServices(services => |
39 | 37 | { |
40 | | - // Remove the real SQL Server DbContext registration |
41 | | - var dbContextDescriptor = services.SingleOrDefault( |
42 | | - d => d.ServiceType == typeof(DbContextOptions<PortalDbContext>)); |
43 | | - if (dbContextDescriptor != null) |
44 | | - { |
45 | | - services.Remove(dbContextDescriptor); |
46 | | - } |
47 | | - |
48 | | - // Add InMemory EF provider instead |
49 | | - services.AddDbContext<PortalDbContext>(options => |
50 | | - options.UseInMemoryDatabase("IntegrationTests")); |
| 38 | + // Replace database services with no-op mocks so startup |
| 39 | + // doesn't require a real SQL Server instance. |
| 40 | + ReplaceWithMock<IDatabaseMigrator>(services); |
| 41 | + ReplaceWithMock<IDatabaseSeeder>(services); |
| 42 | + }); |
| 43 | + } |
51 | 44 |
|
52 | | - // Replace database migrator and seeder with no-ops |
53 | | - var migratorDescriptor = services.SingleOrDefault( |
54 | | - d => d.ServiceType == typeof(IDatabaseMigrator)); |
55 | | - if (migratorDescriptor != null) |
56 | | - { |
57 | | - services.Remove(migratorDescriptor); |
58 | | - } |
59 | | - services.AddScoped(_ => Substitute.For<IDatabaseMigrator>()); |
| 45 | + /// <summary> |
| 46 | + /// Replaces an existing service registration with a no-op NSubstitute mock. |
| 47 | + /// </summary> |
| 48 | + private static void ReplaceWithMock<TService>(IServiceCollection services) where TService : class |
| 49 | + { |
| 50 | + var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(TService)); |
| 51 | + if (descriptor != null) |
| 52 | + { |
| 53 | + services.Remove(descriptor); |
| 54 | + } |
60 | 55 |
|
61 | | - var seederDescriptor = services.SingleOrDefault( |
62 | | - d => d.ServiceType == typeof(IDatabaseSeeder)); |
63 | | - if (seederDescriptor != null) |
64 | | - { |
65 | | - services.Remove(seederDescriptor); |
66 | | - } |
67 | | - services.AddScoped(_ => Substitute.For<IDatabaseSeeder>()); |
| 56 | + services.AddScoped(_ => Substitute.For<TService>()); |
| 57 | + } |
68 | 58 |
|
69 | | - // Override plugin service registrations with mocks. |
70 | | - // These AddSingleton calls come after AddPlugins' TryAddSingleton defaults |
71 | | - // and any MEF-loaded plugins, so they win — last registration wins in DI. |
72 | | - services.AddSingleton(Substitute.For<ISummerEbtCaseService>()); |
73 | | - services.AddSingleton(Substitute.For<IEnrollmentCheckService>()); |
74 | | - }); |
| 59 | + protected override void Dispose(bool disposing) |
| 60 | + { |
| 61 | + Environment.SetEnvironmentVariable("PluginAssemblyPaths__0", null); |
| 62 | + Environment.SetEnvironmentVariable("PluginAssemblyPaths__1", null); |
| 63 | + Environment.SetEnvironmentVariable("JwtSettings__SecretKey", null); |
| 64 | + base.Dispose(disposing); |
75 | 65 | } |
76 | 66 | } |
0 commit comments