Add UserAgent middleware tests for Foundry hosting

This commit is contained in:
Roger Barreto
2026-04-09 15:40:54 +01:00
Unverified
parent 176b8f9b34
commit fed3c06817
3 changed files with 189 additions and 0 deletions
@@ -1,7 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Reflection;
using System.Threading.Tasks;
using Azure.AI.AgentServer.Responses;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -89,6 +93,56 @@ public static class FoundryHostingExtensions
{
ArgumentNullException.ThrowIfNull(endpoints);
endpoints.MapResponsesServer(prefix);
if (endpoints is IApplicationBuilder app)
{
// Ensure the middleware is added to the pipeline
app.UseMiddleware<AgentFrameworkUserAgentMiddleware>();
}
return endpoints;
}
private sealed class AgentFrameworkUserAgentMiddleware(RequestDelegate next)
{
private static readonly string s_userAgentValue = CreateUserAgentValue();
public async Task InvokeAsync(HttpContext context)
{
var headers = context.Request.Headers;
var userAgent = headers.UserAgent.ToString();
if (string.IsNullOrEmpty(userAgent))
{
headers.UserAgent = s_userAgentValue;
}
else if (!userAgent.Contains(s_userAgentValue, StringComparison.OrdinalIgnoreCase))
{
headers.UserAgent = $"{userAgent} {s_userAgentValue}";
}
await next(context).ConfigureAwait(false);
}
private static string CreateUserAgentValue()
{
const string Name = "agent-framework-dotnet";
if (typeof(AgentFrameworkUserAgentMiddleware).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion is string version)
{
int pos = version.IndexOf('+');
if (pos >= 0)
{
version = version.Substring(0, pos);
}
if (version.Length > 0)
{
return $"{Name}/{version}";
}
}
return Name;
}
}
}
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace Microsoft.Agents.AI.Foundry.UnitTests.Hosting;
/// <summary>
/// Tests for the <c>AgentFrameworkUserAgentMiddleware</c> registered by
/// <see cref="FoundryHostingExtensions.MapFoundryResponses"/>.
/// </summary>
public sealed partial class UserAgentMiddlewareTests : IAsyncDisposable
{
private const string VersionedUserAgentPattern = @"agent-framework-dotnet/\d+\.\d+\.\d+";
private WebApplication? _app;
private HttpClient? _httpClient;
public async ValueTask DisposeAsync()
{
this._httpClient?.Dispose();
if (this._app != null)
{
await this._app.DisposeAsync();
}
}
[Fact]
public async Task MapFoundryResponses_NoUserAgentHeader_SetsAgentFrameworkUserAgentAsync()
{
// Arrange
await this.CreateTestServerAsync();
using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua");
// Act
var response = await this._httpClient!.SendAsync(request);
var userAgent = await response.Content.ReadAsStringAsync();
// Assert
Assert.Matches(VersionedUserAgentPattern, userAgent);
}
[Fact]
public async Task MapFoundryResponses_WithExistingUserAgent_AppendsAgentFrameworkUserAgentAsync()
{
// Arrange
await this.CreateTestServerAsync();
using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua");
request.Headers.TryAddWithoutValidation("User-Agent", "MyApp/1.0");
// Act
var response = await this._httpClient!.SendAsync(request);
var userAgent = await response.Content.ReadAsStringAsync();
// Assert
Assert.StartsWith("MyApp/1.0", userAgent);
Assert.Matches(VersionedUserAgentPattern, userAgent);
}
[Fact]
public async Task MapFoundryResponses_AlreadyContainsUserAgent_DoesNotDuplicateAsync()
{
// Arrange
await this.CreateTestServerAsync();
// First request to capture the actual middleware-generated value
using var firstRequest = new HttpRequestMessage(HttpMethod.Get, "/test-ua");
var firstResponse = await this._httpClient!.SendAsync(firstRequest);
var middlewareValue = await firstResponse.Content.ReadAsStringAsync();
// Act: send a second request that already contains the middleware value
using var secondRequest = new HttpRequestMessage(HttpMethod.Get, "/test-ua");
secondRequest.Headers.TryAddWithoutValidation("User-Agent", $"MyApp/2.0 {middlewareValue}");
var secondResponse = await this._httpClient!.SendAsync(secondRequest);
var userAgent = await secondResponse.Content.ReadAsStringAsync();
// Assert: should remain unchanged (no duplication)
Assert.Equal($"MyApp/2.0 {middlewareValue}", userAgent);
Assert.Single(VersionedUserAgentRegex().Matches(userAgent));
}
[Fact]
public async Task MapFoundryResponses_UserAgentValue_ContainsVersionAsync()
{
// Arrange
await this.CreateTestServerAsync();
using var request = new HttpRequestMessage(HttpMethod.Get, "/test-ua");
// Act
var response = await this._httpClient!.SendAsync(request);
var userAgent = await response.Content.ReadAsStringAsync();
// Assert: should match "agent-framework-dotnet/x.y.z" pattern
Assert.Matches(VersionedUserAgentPattern, userAgent);
}
private async Task CreateTestServerAsync()
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
var mockAgent = new Mock<AIAgent>();
builder.Services.AddFoundryResponses(mockAgent.Object);
this._app = builder.Build();
this._app.MapFoundryResponses();
// Test endpoint that echoes the User-Agent header after middleware processing
this._app.MapGet("/test-ua", (HttpContext ctx) =>
Results.Text(ctx.Request.Headers.UserAgent.ToString()));
await this._app.StartAsync();
var testServer = this._app.Services.GetRequiredService<IServer>() as TestServer
?? throw new InvalidOperationException("TestServer not found");
this._httpClient = testServer.CreateClient();
}
[GeneratedRegex(VersionedUserAgentPattern)]
private static partial Regex VersionedUserAgentRegex();
}
@@ -12,6 +12,7 @@
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<PackageReference Include="Azure.AI.AgentServer.Responses" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" />
</ItemGroup>
<!-- Hosting tests only compile on .NET Core TFMs -->