Skip to content

Commit 1a845d8

Browse files
test: fix integration factory disposal and traced redirects
1 parent a264306 commit 1a845d8

3 files changed

Lines changed: 45 additions & 47 deletions

File tree

EssentialCSharp.Web.Tests/FunctionalTests.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ public class FunctionalTests : IntegrationTestBase
1313
[Arguments("/alive")]
1414
public async Task WhenTheApplicationStarts_ItCanLoadLoadPages(string relativeUrl)
1515
{
16-
HttpClient client = CreateRedirectFollowingClient();
17-
using HttpResponseMessage response = await client.GetAsync(relativeUrl);
16+
using HttpResponseMessage response = await GetWithRedirectsAsync(relativeUrl);
1817

1918
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
2019
}
@@ -29,8 +28,7 @@ public async Task WhenTheApplicationStarts_ItCanLoadLoadPages(string relativeUrl
2928
[Arguments("/about?someOtherParam=value")]
3029
public async Task WhenPagesAreAccessed_TheyReturnHtml(string relativeUrl)
3130
{
32-
HttpClient client = CreateRedirectFollowingClient();
33-
using HttpResponseMessage response = await client.GetAsync(relativeUrl);
31+
using HttpResponseMessage response = await GetWithRedirectsAsync(relativeUrl);
3432

3533
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
3634

@@ -45,8 +43,7 @@ public async Task WhenPagesAreAccessed_TheyReturnHtml(string relativeUrl)
4543
[Test]
4644
public async Task WhenTheApplicationStarts_NonExistingPage_GivesCorrectStatusCode()
4745
{
48-
HttpClient client = CreateRedirectFollowingClient();
49-
using HttpResponseMessage response = await client.GetAsync("/non-existing-page1234");
46+
using HttpResponseMessage response = await GetWithRedirectsAsync("/non-existing-page1234");
5047

5148
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.NotFound);
5249
}

EssentialCSharp.Web.Tests/IntegrationTestBase.cs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.AspNetCore.Mvc.Testing;
1+
using System.Net;
22
using Microsoft.Extensions.DependencyInjection;
33
using TUnit.AspNetCore;
44

@@ -7,14 +7,38 @@ namespace EssentialCSharp.Web.Tests;
77
public abstract class IntegrationTestBase : WebApplicationTest<WebApplicationFactory, Program>
88
{
99
/// <summary>
10-
/// Creates an HTTP client with redirect following enabled.
11-
/// NOTE: This bypasses TUnit trace correlation because <see cref="TracedWebApplicationFactory{T}"/>
12-
/// does not expose a <c>CreateClient(WebApplicationFactoryClientOptions)</c> overload.
13-
/// Use <see cref="TUnit.AspNetCore.TracedWebApplicationFactory{T}.CreateClient()"/> for all
14-
/// other tests where AllowAutoRedirect=false is acceptable.
10+
/// Executes a GET request and follows redirect responses while preserving
11+
/// TUnit trace correlation by using <see cref="TracedWebApplicationFactory{T}.CreateClient()"/>.
1512
/// </summary>
16-
protected HttpClient CreateRedirectFollowingClient() =>
17-
Factory.Inner.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = true });
13+
protected async Task<HttpResponseMessage> GetWithRedirectsAsync(string relativeUrl, int maxRedirects = 10)
14+
{
15+
HttpClient client = Factory.CreateClient();
16+
HttpResponseMessage response = await client.GetAsync(relativeUrl);
17+
18+
for (int redirectCount = 0;
19+
redirectCount < maxRedirects && IsRedirectStatusCode(response.StatusCode);
20+
redirectCount++)
21+
{
22+
Uri? location = response.Headers.Location;
23+
if (location is null)
24+
{
25+
return response;
26+
}
27+
28+
response.Dispose();
29+
30+
response = await client.GetAsync(location);
31+
}
32+
33+
return response;
34+
}
35+
36+
private static bool IsRedirectStatusCode(HttpStatusCode statusCode) =>
37+
statusCode == HttpStatusCode.Moved ||
38+
statusCode == HttpStatusCode.Found ||
39+
statusCode == HttpStatusCode.RedirectMethod ||
40+
statusCode == HttpStatusCode.TemporaryRedirect ||
41+
statusCode == HttpStatusCode.PermanentRedirect;
1842

1943
public T InServiceScope<T>(Func<IServiceProvider, T> action)
2044
{

EssentialCSharp.Web.Tests/WebApplicationFactory.cs

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Collections.Concurrent;
21
using System.Data.Common;
32
using EssentialCSharp.Web.Data;
43
using EssentialCSharp.Web.Services;
@@ -16,9 +15,6 @@ public sealed class WebApplicationFactory : TestWebApplicationFactory<Program>
1615
{
1716
private static string SqlConnectionString => $"DataSource=file:{Guid.NewGuid()}?mode=memory&cache=shared";
1817

19-
// Each per-test factory's ConfigureWebHost creates a new connection; track all for disposal.
20-
private readonly ConcurrentBag<SqliteConnection> _connections = [];
21-
2218
protected override void ConfigureWebHost(IWebHostBuilder builder)
2319
{
2420
builder.ConfigureServices(services =>
@@ -55,17 +51,20 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
5551
// to its own connection, not to a shared field that gets overwritten.
5652
SqliteConnection connection = new(SqlConnectionString);
5753
connection.Open();
58-
_connections.Add(connection);
5954

60-
services.AddDbContext<EssentialCSharpWebContext>(options =>
55+
services.AddSingleton<DbConnection>(connection);
56+
57+
services.AddDbContext<EssentialCSharpWebContext>((serviceProvider, options) =>
6158
{
62-
options.UseSqlite(connection);
59+
DbConnection dbConnection = serviceProvider.GetRequiredService<DbConnection>();
60+
options.UseSqlite(dbConnection);
6361
});
6462

65-
using ServiceProvider serviceProvider = services.BuildServiceProvider();
66-
using IServiceScope scope = serviceProvider.CreateScope();
67-
IServiceProvider scopedServices = scope.ServiceProvider;
68-
EssentialCSharpWebContext db = scopedServices.GetRequiredService<EssentialCSharpWebContext>();
63+
DbContextOptions<EssentialCSharpWebContext> dbContextOptions =
64+
new DbContextOptionsBuilder<EssentialCSharpWebContext>()
65+
.UseSqlite(connection)
66+
.Options;
67+
using EssentialCSharpWebContext db = new(dbContextOptions);
6968

7069
db.Database.EnsureCreated();
7170

@@ -75,26 +74,4 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
7574
_ => TestListingSourceCodeServiceHelper.CreateService());
7675
});
7776
}
78-
79-
private int _connectionsDisposed;
80-
81-
public override async ValueTask DisposeAsync()
82-
{
83-
await base.DisposeAsync().ConfigureAwait(false);
84-
if (Interlocked.Exchange(ref _connectionsDisposed, 1) == 0)
85-
{
86-
foreach (SqliteConnection connection in _connections)
87-
await connection.DisposeAsync().ConfigureAwait(false);
88-
}
89-
}
90-
91-
protected override void Dispose(bool disposing)
92-
{
93-
base.Dispose(disposing);
94-
if (disposing && Interlocked.Exchange(ref _connectionsDisposed, 1) == 0)
95-
{
96-
foreach (SqliteConnection connection in _connections)
97-
connection.Dispose();
98-
}
99-
}
10077
}

0 commit comments

Comments
 (0)