Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions TUnit.Aspire.Tests/IntegrationTestFixture.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using Aspire.Hosting.Testing;
using Microsoft.Extensions.DependencyInjection;

namespace TUnit.Aspire.Tests;

/// <summary>
Expand All @@ -10,4 +13,34 @@ public class IntegrationTestFixture : AspireFixture<Projects.TUnit_Aspire_Tests_
protected override TimeSpan ResourceTimeout => TimeSpan.FromSeconds(120);

protected override ResourceWaitBehavior WaitBehavior => ResourceWaitBehavior.AllRunning;

protected override void ConfigureBuilder(IDistributedApplicationTestingBuilder builder)
{
builder.Services.AddSingleton<HttpHandlerInvocationCounter>();
builder.Services.AddTransient<CountingDelegatingHandler>();
builder.Services.ConfigureHttpClientDefaults(http =>
http.AddHttpMessageHandler<CountingDelegatingHandler>());
}

public int HttpHandlerInvocationCount
=> App.Services.GetRequiredService<HttpHandlerInvocationCounter>().Count;
}

internal sealed class HttpHandlerInvocationCounter
{
private int _count;

public int Count => Volatile.Read(ref _count);

public void Increment() => Interlocked.Increment(ref _count);
}

internal sealed class CountingDelegatingHandler(HttpHandlerInvocationCounter counter) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
counter.Increment();
return base.SendAsync(request, cancellationToken);
}
}
17 changes: 17 additions & 0 deletions TUnit.Aspire.Tests/OtlpCorrelationIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,23 @@ public async Task TraceDemo_Request_ProducesNestedCorrelatedLogs()
await Assert.That(output).Contains(nestedMarker);
}

/// <summary>
/// Regression coverage for thomhurst/TUnit#5956 — <c>CreateHttpClient</c> must route through
/// the AppHost's <see cref="IHttpClientFactory"/> so that <see cref="DelegatingHandler"/>s
/// registered via <c>ConfigureHttpClientDefaults</c> in <c>ConfigureBuilder</c> fire.
/// </summary>
[Test]
public async Task CreateHttpClient_Invokes_Registered_DelegatingHandler()
{
var before = fixture.HttpHandlerInvocationCount;

var client = fixture.CreateHttpClient(ServiceName);
using var response = await client.GetAsync("/health");

await Assert.That((int)response.StatusCode).IsEqualTo(200);
await Assert.That(fixture.HttpHandlerInvocationCount).IsGreaterThan(before);
}

/// <summary>
/// Polls <see cref="TestContext.GetStandardOutput"/> until it contains the expected marker
/// or a timeout is reached. The OTLP SDK batches log exports, so there's inherent latency
Expand Down
30 changes: 4 additions & 26 deletions TUnit.Aspire/AspireFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public class AspireFixture<TAppHost> : IAsyncInitializer, IAsyncDisposable
{
private DistributedApplication? _app;
private OtlpReceiver? _otlpReceiver;
private SocketsHttpHandler? _httpHandler;

/// <summary>
/// The running Aspire distributed application.
Expand All @@ -39,33 +38,15 @@ public class AspireFixture<TAppHost> : IAsyncInitializer, IAsyncDisposable
"App not initialized. Ensure InitializeAsync has completed.");

/// <summary>
/// Creates an <see cref="HttpClient"/> for the named resource.
/// When <see cref="EnableTelemetryCollection"/> is <c>true</c>, the returned client
/// automatically propagates W3C <c>traceparent</c> and <c>baggage</c> headers
/// (including <c>tunit.test.id</c>) to the SUT for cross-process correlation.
/// Otherwise, delegates to Aspire's default <c>CreateHttpClient</c>.
/// Creates an <see cref="HttpClient"/> for the named resource via the AppHost's
/// <see cref="IHttpClientFactory"/>, so any <see cref="DelegatingHandler"/> registered
/// in <see cref="ConfigureBuilder"/> participates in the pipeline.
/// </summary>
/// <param name="resourceName">The name of the resource to connect to.</param>
/// <param name="endpointName">Optional endpoint name if the resource exposes multiple endpoints.</param>
/// <returns>An <see cref="HttpClient"/> configured to connect to the resource.</returns>
public HttpClient CreateHttpClient(string resourceName, string? endpointName = null)
{
if (!EnableTelemetryCollection)
{
return App.CreateHttpClient(resourceName, endpointName);
}

_httpHandler ??= new SocketsHttpHandler
{
// Match Aspire's CreateHttpClient behavior: trust dev certs for HTTPS resources
SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true },
};

return new HttpClient(_httpHandler, disposeHandler: false)
{
BaseAddress = App.GetEndpoint(resourceName, endpointName),
};
}
=> App.CreateHttpClient(resourceName, endpointName);

/// <summary>
/// Gets the connection string for the named resource.
Expand Down Expand Up @@ -387,9 +368,6 @@ public virtual async ValueTask DisposeAsync()
_otlpReceiver = null;
}

_httpHandler?.Dispose();
_httpHandler = null;

GC.SuppressFinalize(this);
}

Expand Down
Loading