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
12 changes: 12 additions & 0 deletions tests/Common/Utils/TestConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,16 @@ public static class TestConstants
/// Set to 60 seconds to provide sufficient buffer for slow CI environments.
/// </summary>
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);

/// <summary>
/// Timeout for HttpClient operations in tests.
/// Set to 60 seconds to provide sufficient buffer for slow CI environments.
/// </summary>
public static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(60);

/// <summary>
/// Timeout for short-lived HTTP requests during polling operations.
/// Set to 2 seconds for quick failure detection while polling.
/// </summary>
public static readonly TimeSpan HttpClientPollingTimeout = TimeSpan.FromSeconds(2);
Comment thread
ericstj marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public async ValueTask DisposeAsync()

protected async Task<WebApplication> StartMcpServerAsync(string path = "", string? authScheme = null)
{
// Wait for the OAuth server to be ready before starting the MCP server.
// This prevents race conditions in CI where the OAuth server may not be
// fully initialized when the first test request is made.
await TestOAuthServer.ServerStarted.WaitAsync(TestContext.Current.CancellationToken);

Builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.ValidAudience = $"{McpServerUrl}{path}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public async ValueTask InitializeAsync()
// Wait for server to be ready (retry for up to 30 seconds)
var timeout = TimeSpan.FromSeconds(30);
var stopwatch = Stopwatch.StartNew();
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientPollingTimeout };

while (stopwatch.Elapsed < timeout)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public KestrelInMemoryTest(ITestOutputHelper testOutputHelper)
protected static void ConfigureHttpClient(HttpClient httpClient)
{
httpClient.BaseAddress = new Uri("http://localhost:5000/");
httpClient.Timeout = TimeSpan.FromSeconds(10);
httpClient.Timeout = TestConstants.HttpClientTimeout;
}

public override void Dispose()
Expand Down
21 changes: 20 additions & 1 deletion tests/ModelContextProtocol.TestOAuthServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public sealed class Program

private readonly ILoggerProvider? _loggerProvider;
private readonly IConnectionListenerFactory? _kestrelTransport;
private readonly TaskCompletionSource _serverStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);

/// <summary>
/// Initializes a new instance of the <see cref="Program"/> class with logging and transport parameters.
Expand All @@ -47,6 +48,11 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor
_kestrelTransport = kestrelTransport;
}

/// <summary>
/// Gets a task that completes when the server has started and is ready to accept connections.
/// </summary>
public Task ServerStarted => _serverStarted.Task;

// Track if we've already issued an already-expired token for the CanAuthenticate_WithTokenRefresh test which uses the test-refresh-client registration.
public bool HasRefreshedToken { get; set; }

Expand Down Expand Up @@ -541,7 +547,20 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null)
Console.WriteLine($"Demo Client ID: {clientId}");
Console.WriteLine($"Demo Client Secret: {clientSecret}");

await app.RunAsync(cancellationToken);
await app.StartAsync(cancellationToken);
_serverStarted.TrySetResult();

// Wait until cancellation is requested
try
{
await Task.Delay(Timeout.Infinite, cancellationToken);
}
catch (OperationCanceledException)
{
// Expected when cancellation is requested
}

await app.StopAsync();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Net;
using ModelContextProtocol.Tests.Utils;

namespace ModelContextProtocol.Tests;

Expand Down Expand Up @@ -33,7 +34,7 @@ public async Task StartAsync()
?? throw new InvalidOperationException($"Could not start process for {processStartInfo.FileName} with '{processStartInfo.Arguments}'.");

// Poll until the server is ready (up to 30 seconds)
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientPollingTimeout };
var endpoint = $"http://localhost:{_port}/sse";
var deadline = DateTime.UtcNow.AddSeconds(30);

Expand Down Expand Up @@ -72,7 +73,7 @@ public async ValueTask DisposeAsync()

using var stopProcess = Process.Start(stopInfo)
?? throw new InvalidOperationException($"Could not stop process for {stopInfo.FileName} with '{stopInfo.Arguments}'.");
await stopProcess.WaitForExitAsync(TimeSpan.FromSeconds(10));
await stopProcess.WaitForExitAsync(TestConstants.DefaultTimeout);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public async Task SigInt_DisposesTestServerWithHosting_Gracefully()
// https://github.com/dotnet/runtime/issues/109432, https://github.com/dotnet/runtime/issues/44944
Assert.Equal(0, kill(process.Id, SIGINT));

await process.WaitForExitAsync(TimeSpan.FromSeconds(10));
await process.WaitForExitAsync(TestConstants.DefaultTimeout);

Assert.True(process.HasExited);
Assert.Equal(0, process.ExitCode);
Expand Down