diff --git a/tests/Common/Utils/TestConstants.cs b/tests/Common/Utils/TestConstants.cs index eae9a54b1..4a337df36 100644 --- a/tests/Common/Utils/TestConstants.cs +++ b/tests/Common/Utils/TestConstants.cs @@ -10,4 +10,16 @@ public static class TestConstants /// Set to 60 seconds to provide sufficient buffer for slow CI environments. /// public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); + + /// + /// Timeout for HttpClient operations in tests. + /// Set to 60 seconds to provide sufficient buffer for slow CI environments. + /// + public static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(60); + + /// + /// Timeout for short-lived HTTP requests during polling operations. + /// Set to 2 seconds for quick failure detection while polling. + /// + public static readonly TimeSpan HttpClientPollingTimeout = TimeSpan.FromSeconds(2); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs index 52b5616d0..5d66ee80e 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs @@ -83,6 +83,11 @@ public async ValueTask DisposeAsync() protected async Task 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(JwtBearerDefaults.AuthenticationScheme, options => { options.TokenValidationParameters.ValidAudience = $"{McpServerUrl}{path}"; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs index 031a67a18..02a98b890 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs @@ -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) { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs b/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs index c93a27650..0bbff49b4 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs @@ -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() diff --git a/tests/ModelContextProtocol.TestOAuthServer/Program.cs b/tests/ModelContextProtocol.TestOAuthServer/Program.cs index e13c731de..364836311 100644 --- a/tests/ModelContextProtocol.TestOAuthServer/Program.cs +++ b/tests/ModelContextProtocol.TestOAuthServer/Program.cs @@ -33,6 +33,7 @@ public sealed class Program private readonly ILoggerProvider? _loggerProvider; private readonly IConnectionListenerFactory? _kestrelTransport; + private readonly TaskCompletionSource _serverStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); /// /// Initializes a new instance of the class with logging and transport parameters. @@ -47,6 +48,11 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor _kestrelTransport = kestrelTransport; } + /// + /// Gets a task that completes when the server has started and is ready to accept connections. + /// + 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; } @@ -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(); } /// diff --git a/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs b/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs index f12aff5e5..e579ff9f0 100644 --- a/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs +++ b/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Net; +using ModelContextProtocol.Tests.Utils; namespace ModelContextProtocol.Tests; @@ -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); @@ -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) { diff --git a/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs b/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs index d14c376c1..88604f533 100644 --- a/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs @@ -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);