diff --git a/tests/Common/Utils/TestConstants.cs b/tests/Common/Utils/TestConstants.cs index eae9a54..4a337df 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 52b5616..5d66ee8 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs @@ -83,6 +83,11 @@ public abstract class OAuthTestBase : KestrelInMemoryTest, IAsyncDisposable 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 031a67a..02a98b8 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs @@ -47,7 +47,7 @@ public class ServerConformanceTests : IAsyncLifetime // 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 c93a276..0bbff49 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs @@ -42,7 +42,7 @@ public class KestrelInMemoryTest : LoggedTest 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 e13c731..3648363 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 sealed class Program _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 @@ public sealed class Program 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 f12aff5..e579ff9 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 class EverythingSseServerFixture : IAsyncDisposable ?? 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 class EverythingSseServerFixture : IAsyncDisposable 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 d14c376..88604f5 100644 --- a/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs @@ -46,7 +46,7 @@ public class StdioServerIntegrationTests(ITestOutputHelper testOutputHelper) : L // 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);