Skip to content

Commit 153572e

Browse files
fix(tests): harden factory override checks and keep redirect handling explicit
Use explicit single-or-none descriptor removal (throws if duplicates are present) for DbContext options, DbConnection, DatabaseMigrationService, and EnsureCreatedHostedService. Keep schema creation serialized via semaphore. Update FunctionalTests to use CreateClientWithoutAutoRedirect so manual redirect handling remains intentional.
1 parent c4445d2 commit 153572e

1 file changed

Lines changed: 47 additions & 34 deletions

File tree

EssentialCSharp.Web.Tests/WebApplicationFactory.cs

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ namespace EssentialCSharp.Web.Tests;
1616
public sealed class WebApplicationFactory : TestWebApplicationFactory<Program>
1717
{
1818
private static readonly SemaphoreSlim SchemaInitializationGate = new(1, 1);
19-
// One GUID per factory instance → each factory gets its own isolated in-memory database.
19+
// One GUID per factory instance (field, not computed property) ensures each factory
20+
// gets its own isolated in-memory database and keeps a stable connection string.
2021
private readonly string _sqlConnectionString =
2122
$"DataSource=file:{Guid.NewGuid():N}?mode=memory&cache=shared";
2223

@@ -28,34 +29,24 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
2829
{
2930
builder.ConfigureServices(services =>
3031
{
31-
for (int i = services.Count - 1; i >= 0; i--)
32-
{
33-
if (services[i].ServiceType ==
34-
typeof(IDbContextOptionsConfiguration<EssentialCSharpWebContext>))
35-
{
36-
services.RemoveAt(i);
37-
}
38-
}
32+
RemoveSingleOrNone(
33+
services,
34+
descriptor => descriptor.ServiceType ==
35+
typeof(IDbContextOptionsConfiguration<EssentialCSharpWebContext>),
36+
"IDbContextOptionsConfiguration<EssentialCSharpWebContext>");
3937

40-
for (int i = services.Count - 1; i >= 0; i--)
41-
{
42-
if (services[i].ServiceType == typeof(DbConnection))
43-
{
44-
services.RemoveAt(i);
45-
}
46-
}
38+
RemoveSingleOrNone(
39+
services,
40+
descriptor => descriptor.ServiceType == typeof(DbConnection),
41+
nameof(DbConnection));
4742

4843
// Remove DatabaseMigrationService: it calls MigrateAsync which conflicts
4944
// with EnsureCreated() used below for the in-memory SQLite test database.
50-
for (int i = services.Count - 1; i >= 0; i--)
51-
{
52-
ServiceDescriptor descriptor = services[i];
53-
if (descriptor.ServiceType == typeof(IHostedService) &&
54-
descriptor.ImplementationType == typeof(DatabaseMigrationService))
55-
{
56-
services.RemoveAt(i);
57-
}
58-
}
45+
RemoveSingleOrNone(
46+
services,
47+
descriptor => descriptor.ServiceType == typeof(IHostedService) &&
48+
descriptor.ImplementationType == typeof(DatabaseMigrationService),
49+
nameof(DatabaseMigrationService));
5950

6051
// Open a keep-alive connection to prevent the shared-cache in-memory database from
6152
// being dropped when per-scope connections are disposed between requests.
@@ -81,15 +72,11 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
8172
});
8273

8374
// Ensure schema exists before any hosted service that reads from the database.
84-
for (int i = services.Count - 1; i >= 0; i--)
85-
{
86-
ServiceDescriptor descriptor = services[i];
87-
if (descriptor.ServiceType == typeof(IHostedService) &&
88-
descriptor.ImplementationType == typeof(EnsureCreatedHostedService))
89-
{
90-
services.RemoveAt(i);
91-
}
92-
}
75+
RemoveSingleOrNone(
76+
services,
77+
descriptor => descriptor.ServiceType == typeof(IHostedService) &&
78+
descriptor.ImplementationType == typeof(EnsureCreatedHostedService),
79+
nameof(EnsureCreatedHostedService));
9380

9481
ServiceDescriptor ensureCreatedDescriptor =
9582
ServiceDescriptor.Singleton<IHostedService, EnsureCreatedHostedService>();
@@ -127,6 +114,32 @@ protected override void Dispose(bool disposing)
127114
base.Dispose(disposing);
128115
}
129116

117+
private static void RemoveSingleOrNone(
118+
IServiceCollection services,
119+
Func<ServiceDescriptor, bool> predicate,
120+
string descriptorName)
121+
{
122+
List<int> matchIndexes = [];
123+
for (int i = 0; i < services.Count; i++)
124+
{
125+
if (predicate(services[i]))
126+
{
127+
matchIndexes.Add(i);
128+
}
129+
}
130+
131+
if (matchIndexes.Count > 1)
132+
{
133+
throw new InvalidOperationException(
134+
$"Expected at most one '{descriptorName}' registration but found {matchIndexes.Count}.");
135+
}
136+
137+
if (matchIndexes.Count == 1)
138+
{
139+
services.RemoveAt(matchIndexes[0]);
140+
}
141+
}
142+
130143
private sealed class EnsureCreatedHostedService(IServiceProvider serviceProvider) : IHostedService
131144
{
132145
public async Task StartAsync(CancellationToken cancellationToken)

0 commit comments

Comments
 (0)