diff --git a/src/Microsoft.AspNetCore.TestHost/ClientHandler.cs b/src/Microsoft.AspNetCore.TestHost/ClientHandler.cs
index f74b774785..2109809d1a 100644
--- a/src/Microsoft.AspNetCore.TestHost/ClientHandler.cs
+++ b/src/Microsoft.AspNetCore.TestHost/ClientHandler.cs
@@ -33,12 +33,7 @@ namespace Microsoft.AspNetCore.TestHost
/// The .
public ClientHandler(PathString pathBase, IHttpApplication application)
{
- if (application == null)
- {
- throw new ArgumentNullException(nameof(application));
- }
-
- _application = application;
+ _application = application ?? throw new ArgumentNullException(nameof(application));
// PathString.StartsWithSegments that we use below requires the base path to not end in a slash.
if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
@@ -64,187 +59,74 @@ namespace Microsoft.AspNetCore.TestHost
throw new ArgumentNullException(nameof(request));
}
- var state = new RequestState(request, _pathBase, _application);
+ var contextBuilder = new HttpContextBuilder(_application);
+
+ Stream responseBody = null;
var requestContent = request.Content ?? new StreamContent(Stream.Null);
var body = await requestContent.ReadAsStreamAsync();
- if (body.CanSeek)
+ contextBuilder.Configure(context =>
{
- // This body may have been consumed before, rewind it.
- body.Seek(0, SeekOrigin.Begin);
- }
- state.Context.HttpContext.Request.Body = body;
- var registration = cancellationToken.Register(state.AbortRequest);
+ var req = context.Request;
- // Async offload, don't let the test code block the caller.
- var offload = Task.Factory.StartNew(async () =>
- {
- try
- {
- await _application.ProcessRequestAsync(state.Context);
- await state.CompleteResponseAsync();
- state.ServerCleanup(exception: null);
- }
- catch (Exception ex)
- {
- state.Abort(ex);
- state.ServerCleanup(ex);
- }
- finally
- {
- registration.Dispose();
- }
- });
-
- return await state.ResponseTask.ConfigureAwait(false);
- }
-
- private class RequestState
- {
- private readonly HttpRequestMessage _request;
- private readonly IHttpApplication _application;
- private TaskCompletionSource _responseTcs;
- private ResponseStream _responseStream;
- private ResponseFeature _responseFeature;
- private CancellationTokenSource _requestAbortedSource;
- private bool _pipelineFinished;
-
- internal RequestState(HttpRequestMessage request, PathString pathBase, IHttpApplication application)
- {
- _request = request;
- _application = application;
- _responseTcs = new TaskCompletionSource();
- _requestAbortedSource = new CancellationTokenSource();
- _pipelineFinished = false;
+ req.Protocol = "HTTP/" + request.Version.ToString(fieldCount: 2);
+ req.Method = request.Method.ToString();
+ req.Scheme = request.RequestUri.Scheme;
+ req.Host = HostString.FromUriComponent(request.RequestUri);
if (request.RequestUri.IsDefaultPort)
{
- request.Headers.Host = request.RequestUri.Host;
+ req.Host = new HostString(req.Host.Host);
}
- else
+
+ req.Path = PathString.FromUriComponent(request.RequestUri);
+ req.PathBase = PathString.Empty;
+ if (req.Path.StartsWithSegments(_pathBase, out var remainder))
{
- request.Headers.Host = request.RequestUri.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped);
+ req.Path = remainder;
+ req.PathBase = _pathBase;
}
-
- var contextFeatures = new FeatureCollection();
- var requestFeature = new RequestFeature();
- contextFeatures.Set(requestFeature);
- _responseFeature = new ResponseFeature();
- contextFeatures.Set(_responseFeature);
- var requestLifetimeFeature = new HttpRequestLifetimeFeature();
- contextFeatures.Set(requestLifetimeFeature);
-
- requestFeature.Protocol = "HTTP/" + request.Version.ToString(fieldCount: 2);
- requestFeature.Scheme = request.RequestUri.Scheme;
- requestFeature.Method = request.Method.ToString();
-
- var fullPath = PathString.FromUriComponent(request.RequestUri);
- PathString remainder;
- if (fullPath.StartsWithSegments(pathBase, out remainder))
- {
- requestFeature.PathBase = pathBase.Value;
- requestFeature.Path = remainder.Value;
- }
- else
- {
- requestFeature.PathBase = string.Empty;
- requestFeature.Path = fullPath.Value;
- }
-
- requestFeature.QueryString = QueryString.FromUriComponent(request.RequestUri).Value;
+ req.QueryString = QueryString.FromUriComponent(request.RequestUri);
foreach (var header in request.Headers)
{
- requestFeature.Headers.Append(header.Key, header.Value.ToArray());
+ req.Headers.Append(header.Key, header.Value.ToArray());
}
- var requestContent = request.Content;
if (requestContent != null)
{
- foreach (var header in request.Content.Headers)
+ foreach (var header in requestContent.Headers)
{
- requestFeature.Headers.Append(header.Key, header.Value.ToArray());
+ req.Headers.Append(header.Key, header.Value.ToArray());
}
}
- _responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest);
- _responseFeature.Body = _responseStream;
- _responseFeature.StatusCode = 200;
- requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token;
-
- Context = application.CreateContext(contextFeatures);
- }
-
- public Context Context { get; private set; }
-
- public Task ResponseTask
- {
- get { return _responseTcs.Task; }
- }
-
- internal void AbortRequest()
- {
- if (!_pipelineFinished)
+ if (body.CanSeek)
{
- _requestAbortedSource.Cancel();
+ // This body may have been consumed before, rewind it.
+ body.Seek(0, SeekOrigin.Begin);
}
- _responseStream.Complete();
- }
+ req.Body = body;
- internal async Task CompleteResponseAsync()
- {
- _pipelineFinished = true;
- await ReturnResponseMessageAsync();
- _responseStream.Complete();
- await _responseFeature.FireOnResponseCompletedAsync();
- }
+ responseBody = context.Response.Body;
+ });
- internal async Task ReturnResponseMessageAsync()
+ var httpContext = await contextBuilder.SendAsync(cancellationToken);
+
+ var response = new HttpResponseMessage();
+ response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
+ response.ReasonPhrase = httpContext.Features.Get().ReasonPhrase;
+ response.RequestMessage = request;
+
+ response.Content = new StreamContent(responseBody);
+
+ foreach (var header in httpContext.Response.Headers)
{
- // Check if the response has already started because the TrySetResult below could happen a bit late
- // (as it happens on a different thread) by which point the CompleteResponseAsync could run and calls this
- // method again.
- if (!Context.HttpContext.Response.HasStarted)
+ if (!response.Headers.TryAddWithoutValidation(header.Key, (IEnumerable)header.Value))
{
- var response = await GenerateResponseAsync();
- // Dispatch, as TrySetResult will synchronously execute the waiters callback and block our Write.
- var setResult = Task.Factory.StartNew(() => _responseTcs.TrySetResult(response));
+ bool success = response.Content.Headers.TryAddWithoutValidation(header.Key, (IEnumerable)header.Value);
+ Contract.Assert(success, "Bad header");
}
}
-
- private async Task GenerateResponseAsync()
- {
- await _responseFeature.FireOnSendingHeadersAsync();
- var httpContext = Context.HttpContext;
-
- var response = new HttpResponseMessage();
- response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
- response.ReasonPhrase = httpContext.Features.Get().ReasonPhrase;
- response.RequestMessage = _request;
- // response.Version = owinResponse.Protocol;
-
- response.Content = new StreamContent(_responseStream);
-
- foreach (var header in httpContext.Response.Headers)
- {
- if (!response.Headers.TryAddWithoutValidation(header.Key, (IEnumerable)header.Value))
- {
- bool success = response.Content.Headers.TryAddWithoutValidation(header.Key, (IEnumerable)header.Value);
- Contract.Assert(success, "Bad header");
- }
- }
- return response;
- }
-
- internal void Abort(Exception exception)
- {
- _pipelineFinished = true;
- _responseStream.Abort(exception);
- _responseTcs.TrySetException(exception);
- }
-
- internal void ServerCleanup(Exception exception)
- {
- _application.DisposeContext(Context, exception);
- }
+ return response;
}
}
}
diff --git a/src/Microsoft.AspNetCore.TestHost/HttpContextBuilder.cs b/src/Microsoft.AspNetCore.TestHost/HttpContextBuilder.cs
new file mode 100644
index 0000000000..8c69ddd0e9
--- /dev/null
+++ b/src/Microsoft.AspNetCore.TestHost/HttpContextBuilder.cs
@@ -0,0 +1,124 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using static Microsoft.AspNetCore.Hosting.Internal.HostingApplication;
+
+namespace Microsoft.AspNetCore.TestHost
+{
+ internal class HttpContextBuilder
+ {
+ private readonly IHttpApplication _application;
+ private readonly HttpContext _httpContext;
+
+ private TaskCompletionSource _responseTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ private ResponseStream _responseStream;
+ private ResponseFeature _responseFeature = new ResponseFeature();
+ private CancellationTokenSource _requestAbortedSource = new CancellationTokenSource();
+ private bool _pipelineFinished;
+ private Context _testContext;
+
+ internal HttpContextBuilder(IHttpApplication application)
+ {
+ _application = application ?? throw new ArgumentNullException(nameof(application));
+ _httpContext = new DefaultHttpContext();
+
+ var request = _httpContext.Request;
+ request.Protocol = "HTTP/1.1";
+ request.Method = HttpMethods.Get;
+
+ _httpContext.Features.Set(_responseFeature);
+ var requestLifetimeFeature = new HttpRequestLifetimeFeature();
+ requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token;
+ _httpContext.Features.Set(requestLifetimeFeature);
+
+ _responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest);
+ _responseFeature.Body = _responseStream;
+ }
+
+ internal void Configure(Action configureContext)
+ {
+ if (configureContext == null)
+ {
+ throw new ArgumentNullException(nameof(configureContext));
+ }
+
+ configureContext(_httpContext);
+ }
+
+ ///
+ /// Start processing the request.
+ ///
+ ///
+ internal Task SendAsync(CancellationToken cancellationToken)
+ {
+ var registration = cancellationToken.Register(AbortRequest);
+
+ _testContext = _application.CreateContext(_httpContext.Features);
+
+ // Async offload, don't let the test code block the caller.
+ _ = Task.Factory.StartNew(async () =>
+ {
+ try
+ {
+ await _application.ProcessRequestAsync(_testContext);
+ await CompleteResponseAsync();
+ _application.DisposeContext(_testContext, exception: null);
+ }
+ catch (Exception ex)
+ {
+ Abort(ex);
+ _application.DisposeContext(_testContext, ex);
+ }
+ finally
+ {
+ registration.Dispose();
+ }
+ });
+
+ return _responseTcs.Task;
+ }
+
+ internal void AbortRequest()
+ {
+ if (!_pipelineFinished)
+ {
+ _requestAbortedSource.Cancel();
+ }
+ _responseStream.Complete();
+ }
+
+ internal async Task CompleteResponseAsync()
+ {
+ _pipelineFinished = true;
+ await ReturnResponseMessageAsync();
+ _responseStream.Complete();
+ await _responseFeature.FireOnResponseCompletedAsync();
+ }
+
+ internal async Task ReturnResponseMessageAsync()
+ {
+ // Check if the response has already started because the TrySetResult below could happen a bit late
+ // (as it happens on a different thread) by which point the CompleteResponseAsync could run and calls this
+ // method again.
+ if (!_responseFeature.HasStarted)
+ {
+ // Sets HasStarted
+ await _responseFeature.FireOnSendingHeadersAsync();
+ _responseTcs.TrySetResult(_httpContext);
+ }
+ }
+
+ internal void Abort(Exception exception)
+ {
+ _pipelineFinished = true;
+ _responseStream.Abort(exception);
+ _responseTcs.TrySetException(exception);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.TestHost/TestServer.cs b/src/Microsoft.AspNetCore.TestHost/TestServer.cs
index af65de3556..398a575d9d 100644
--- a/src/Microsoft.AspNetCore.TestHost/TestServer.cs
+++ b/src/Microsoft.AspNetCore.TestHost/TestServer.cs
@@ -82,6 +82,38 @@ namespace Microsoft.AspNetCore.TestHost
return new RequestBuilder(this, path);
}
+ ///
+ /// Creates, configures, sends, and returns a . This completes as soon as the response is started.
+ ///
+ ///
+ public async Task SendAsync(Action configureContext, CancellationToken cancellationToken = default)
+ {
+ if (configureContext == null)
+ {
+ throw new ArgumentNullException(nameof(configureContext));
+ }
+
+ var builder = new HttpContextBuilder(_application);
+ builder.Configure(context =>
+ {
+ var request = context.Request;
+ request.Scheme = BaseAddress.Scheme;
+ request.Host = HostString.FromUriComponent(BaseAddress);
+ if (BaseAddress.IsDefaultPort)
+ {
+ request.Host = new HostString(request.Host.Host);
+ }
+ var pathBase = PathString.FromUriComponent(BaseAddress);
+ if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
+ {
+ pathBase = new PathString(pathBase.Value.Substring(0, pathBase.Value.Length - 1));
+ }
+ request.PathBase = pathBase;
+ });
+ builder.Configure(configureContext);
+ return await builder.SendAsync(cancellationToken).ConfigureAwait(false);
+ }
+
public void Dispose()
{
if (!_disposed)
diff --git a/src/Microsoft.AspNetCore.TestHost/WebSocketClient.cs b/src/Microsoft.AspNetCore.TestHost/WebSocketClient.cs
index 3d267f3a1b..e3deb670a5 100644
--- a/src/Microsoft.AspNetCore.TestHost/WebSocketClient.cs
+++ b/src/Microsoft.AspNetCore.TestHost/WebSocketClient.cs
@@ -22,12 +22,7 @@ namespace Microsoft.AspNetCore.TestHost
internal WebSocketClient(PathString pathBase, IHttpApplication application)
{
- if (application == null)
- {
- throw new ArgumentNullException(nameof(application));
- }
-
- _application = application;
+ _application = application ?? throw new ArgumentNullException(nameof(application));
// PathString.StartsWithSegments that we use below requires the base path to not end in a slash.
if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
@@ -53,79 +48,21 @@ namespace Microsoft.AspNetCore.TestHost
public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
{
- var state = new RequestState(uri, _pathBase, cancellationToken, _application);
-
- if (ConfigureRequest != null)
+ WebSocketFeature webSocketFeature = null;
+ var contextBuilder = new HttpContextBuilder(_application);
+ contextBuilder.Configure(context =>
{
- ConfigureRequest(state.Context.HttpContext.Request);
- }
-
- // Async offload, don't let the test code block the caller.
- var offload = Task.Factory.StartNew(async () =>
- {
- try
- {
- await _application.ProcessRequestAsync(state.Context);
- state.PipelineComplete();
- state.ServerCleanup(exception: null);
- }
- catch (Exception ex)
- {
- state.PipelineFailed(ex);
- state.ServerCleanup(ex);
- }
- finally
- {
- state.Dispose();
- }
- });
-
- return await state.WebSocketTask;
- }
-
- private class RequestState : IDisposable, IHttpWebSocketFeature
- {
- private readonly IHttpApplication _application;
- private TaskCompletionSource _clientWebSocketTcs;
- private CancellationTokenRegistration _cancellationTokenRegistration;
- private WebSocket _serverWebSocket;
-
- public Context Context { get; private set; }
- public Task WebSocketTask { get { return _clientWebSocketTcs.Task; } }
-
- public RequestState(Uri uri, PathString pathBase, CancellationToken cancellationToken, IHttpApplication application)
- {
- _clientWebSocketTcs = new TaskCompletionSource();
- _cancellationTokenRegistration = cancellationToken.Register(
- () => _clientWebSocketTcs.TrySetCanceled(cancellationToken));
- _application = application;
-
- // HttpContext
- var contextFeatures = new FeatureCollection();
- contextFeatures.Set(new RequestFeature());
- contextFeatures.Set(new ResponseFeature());
- Context = _application.CreateContext(contextFeatures);
- var httpContext = Context.HttpContext;
-
- // Request
- var request = httpContext.Request;
- request.Protocol = "HTTP/1.1";
+ var request = context.Request;
var scheme = uri.Scheme;
scheme = (scheme == "ws") ? "http" : scheme;
scheme = (scheme == "wss") ? "https" : scheme;
request.Scheme = scheme;
- request.Method = "GET";
- var fullPath = PathString.FromUriComponent(uri);
- PathString remainder;
- if (fullPath.StartsWithSegments(pathBase, out remainder))
+ request.Path = PathString.FromUriComponent(uri);
+ request.PathBase = PathString.Empty;
+ if (request.Path.StartsWithSegments(_pathBase, out var remainder))
{
- request.PathBase = pathBase;
request.Path = remainder;
- }
- else
- {
- request.PathBase = PathString.Empty;
- request.Path = fullPath;
+ request.PathBase = _pathBase;
}
request.QueryString = QueryString.FromUriComponent(uri);
request.Headers.Add("Connection", new string[] { "Upgrade" });
@@ -134,70 +71,63 @@ namespace Microsoft.AspNetCore.TestHost
request.Headers.Add("Sec-WebSocket-Key", new string[] { CreateRequestKey() });
request.Body = Stream.Null;
- // Response
- var response = httpContext.Response;
- response.Body = Stream.Null;
- response.StatusCode = 200;
-
// WebSocket
- httpContext.Features.Set(this);
- }
+ webSocketFeature = new WebSocketFeature(context);
+ context.Features.Set(webSocketFeature);
- public void PipelineComplete()
+ ConfigureRequest?.Invoke(context.Request);
+ });
+
+ var httpContext = await contextBuilder.SendAsync(cancellationToken);
+
+ if (httpContext.Response.StatusCode != StatusCodes.Status101SwitchingProtocols)
{
- PipelineFailed(new InvalidOperationException("Incomplete handshake, status code: " + Context.HttpContext.Response.StatusCode));
+ throw new InvalidOperationException("Incomplete handshake, status code: " + httpContext.Response.StatusCode);
}
-
- public void PipelineFailed(Exception ex)
+ if (webSocketFeature.ClientWebSocket == null)
{
- _clientWebSocketTcs.TrySetException(new InvalidOperationException("The websocket was not accepted.", ex));
+ throw new InvalidOperationException("Incomplete handshake");
}
- public void Dispose()
+ return webSocketFeature.ClientWebSocket;
+ }
+
+ private string CreateRequestKey()
+ {
+ byte[] data = new byte[16];
+ var rng = RandomNumberGenerator.Create();
+ rng.GetBytes(data);
+ return Convert.ToBase64String(data);
+ }
+
+ private class WebSocketFeature : IHttpWebSocketFeature
+ {
+ private readonly HttpContext _httpContext;
+
+ public WebSocketFeature(HttpContext context)
{
- if (_serverWebSocket != null)
- {
- _serverWebSocket.Dispose();
- }
+ _httpContext = context;
}
- internal void ServerCleanup(Exception exception)
- {
- _application.DisposeContext(Context, exception);
- }
+ bool IHttpWebSocketFeature.IsWebSocketRequest => true;
- private string CreateRequestKey()
- {
- byte[] data = new byte[16];
- var rng = RandomNumberGenerator.Create();
- rng.GetBytes(data);
- return Convert.ToBase64String(data);
- }
+ public WebSocket ClientWebSocket { get; private set; }
- bool IHttpWebSocketFeature.IsWebSocketRequest
- {
- get
- {
- return true;
- }
- }
+ public WebSocket ServerWebSocket { get; private set; }
- Task IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
+ async Task IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
{
var websockets = TestWebSocket.CreatePair(context.SubProtocol);
- if (_clientWebSocketTcs.TrySetResult(websockets.Item1))
+ if (_httpContext.Response.HasStarted)
{
- Context.HttpContext.Response.StatusCode = StatusCodes.Status101SwitchingProtocols;
- _serverWebSocket = websockets.Item2;
- return Task.FromResult(_serverWebSocket);
- }
- else
- {
- Context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
- websockets.Item1.Dispose();
- websockets.Item2.Dispose();
- return _clientWebSocketTcs.Task; // Canceled or Faulted - no result
+ throw new InvalidOperationException("The response has already started");
}
+
+ _httpContext.Response.StatusCode = StatusCodes.Status101SwitchingProtocols;
+ ClientWebSocket = websockets.Item1;
+ ServerWebSocket = websockets.Item2;
+ await _httpContext.Response.Body.FlushAsync(_httpContext.RequestAborted); // Send headers to the client
+ return ServerWebSocket;
}
}
}
diff --git a/test/Microsoft.AspNetCore.TestHost.Tests/ClientHandlerTests.cs b/test/Microsoft.AspNetCore.TestHost.Tests/ClientHandlerTests.cs
index 6f57a5fdb7..21da6919b5 100644
--- a/test/Microsoft.AspNetCore.TestHost.Tests/ClientHandlerTests.cs
+++ b/test/Microsoft.AspNetCore.TestHost.Tests/ClientHandlerTests.cs
@@ -87,8 +87,7 @@ namespace Microsoft.AspNetCore.TestHost
return httpClient.GetAsync("https://example.com/");
}
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+ [Fact]
public async Task ResubmitRequestWorks()
{
int requestCount = 1;
@@ -112,8 +111,7 @@ namespace Microsoft.AspNetCore.TestHost
Assert.Equal("TestValue:2", response.Headers.GetValues("TestHeader").First());
}
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+ [Fact]
public async Task MiddlewareOnlySetsHeaders()
{
var handler = new ClientHandler(PathString.Empty, new DummyApplication(context =>
@@ -126,8 +124,7 @@ namespace Microsoft.AspNetCore.TestHost
Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First());
}
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+ [Fact]
public async Task BlockingMiddlewareShouldNotBlockClient()
{
ManualResetEvent block = new ManualResetEvent(false);
@@ -144,8 +141,7 @@ namespace Microsoft.AspNetCore.TestHost
HttpResponseMessage response = await task;
}
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+ [Fact]
public async Task HeadersAvailableBeforeBodyFinished()
{
ManualResetEvent block = new ManualResetEvent(false);
@@ -164,8 +160,7 @@ namespace Microsoft.AspNetCore.TestHost
Assert.Equal("BodyStarted,BodyFinished", await response.Content.ReadAsStringAsync());
}
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+ [Fact]
public async Task FlushSendsHeaders()
{
ManualResetEvent block = new ManualResetEvent(false);
@@ -184,8 +179,7 @@ namespace Microsoft.AspNetCore.TestHost
Assert.Equal("BodyFinished", await response.Content.ReadAsStringAsync());
}
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+ [Fact]
public async Task ClientDisposalCloses()
{
ManualResetEvent block = new ManualResetEvent(false);
@@ -210,8 +204,7 @@ namespace Microsoft.AspNetCore.TestHost
block.Set();
}
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+ [Fact]
public async Task ClientCancellationAborts()
{
ManualResetEvent block = new ManualResetEvent(false);
@@ -249,8 +242,7 @@ namespace Microsoft.AspNetCore.TestHost
HttpCompletionOption.ResponseHeadersRead));
}
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+ [Fact]
public async Task ExceptionAfterFirstWriteIsReported()
{
ManualResetEvent block = new ManualResetEvent(false);
@@ -326,10 +318,8 @@ namespace Microsoft.AspNetCore.TestHost
return Task.FromResult(0);
}
}
-
-
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
+
+ [Fact]
public async Task ClientHandlerCreateContextWithDefaultRequestParameters()
{
// This logger will attempt to access information from HttpRequest once the HttpContext is created
diff --git a/test/Microsoft.AspNetCore.TestHost.Tests/HttpContextBuilderTests.cs b/test/Microsoft.AspNetCore.TestHost.Tests/HttpContextBuilderTests.cs
new file mode 100644
index 0000000000..e7744ef449
--- /dev/null
+++ b/test/Microsoft.AspNetCore.TestHost.Tests/HttpContextBuilderTests.cs
@@ -0,0 +1,302 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace Microsoft.AspNetCore.TestHost
+{
+ public class HttpContextBuilderTests
+ {
+ [Fact]
+ public async Task ExpectedValuesAreAvailable()
+ {
+ var builder = new WebHostBuilder().Configure(app => { });
+ var server = new TestServer(builder);
+ server.BaseAddress = new Uri("https://example.com/A/Path/");
+ var context = await server.SendAsync(c =>
+ {
+ c.Request.Method = HttpMethods.Post;
+ c.Request.Path = "/and/file.txt";
+ c.Request.QueryString = new QueryString("?and=query");
+ });
+
+ Assert.True(context.RequestAborted.CanBeCanceled);
+ Assert.Equal("HTTP/1.1", context.Request.Protocol);
+ Assert.Equal("POST", context.Request.Method);
+ Assert.Equal("https", context.Request.Scheme);
+ Assert.Equal("example.com", context.Request.Host.Value);
+ Assert.Equal("/A/Path", context.Request.PathBase.Value);
+ Assert.Equal("/and/file.txt", context.Request.Path.Value);
+ Assert.Equal("?and=query", context.Request.QueryString.Value);
+ Assert.NotNull(context.Request.Body);
+ Assert.NotNull(context.Request.Headers);
+ Assert.NotNull(context.Response.Headers);
+ Assert.NotNull(context.Response.Body);
+ Assert.Equal(404, context.Response.StatusCode);
+ Assert.Null(context.Features.Get().ReasonPhrase);
+ }
+
+ [Fact]
+ public async Task SingleSlashNotMovedToPathBase()
+ {
+ var builder = new WebHostBuilder().Configure(app => { });
+ var server = new TestServer(builder);
+ var context = await server.SendAsync(c =>
+ {
+ c.Request.Path = "/";
+ });
+
+ Assert.Equal("", context.Request.PathBase.Value);
+ Assert.Equal("/", context.Request.Path.Value);
+ }
+
+ [Fact]
+ public async Task MiddlewareOnlySetsHeaders()
+ {
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(c =>
+ {
+ c.Response.Headers["TestHeader"] = "TestValue";
+ return Task.FromResult(0);
+ });
+ });
+ var server = new TestServer(builder);
+ var context = await server.SendAsync(c => { });
+
+ Assert.Equal("TestValue", context.Response.Headers["TestHeader"]);
+ }
+
+ [Fact]
+ public async Task BlockingMiddlewareShouldNotBlockClient()
+ {
+ var block = new ManualResetEvent(false);
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(c =>
+ {
+ block.WaitOne();
+ return Task.FromResult(0);
+ });
+ });
+ var server = new TestServer(builder);
+ var task = server.SendAsync(c => { });
+
+ Assert.False(task.IsCompleted);
+ Assert.False(task.Wait(50));
+ block.Set();
+ var context = await task;
+ }
+
+ [Fact]
+ public async Task HeadersAvailableBeforeBodyFinished()
+ {
+ var block = new ManualResetEvent(false);
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(async c =>
+ {
+ c.Response.Headers["TestHeader"] = "TestValue";
+ await c.Response.WriteAsync("BodyStarted,");
+ block.WaitOne();
+ await c.Response.WriteAsync("BodyFinished");
+ });
+ });
+ var server = new TestServer(builder);
+ var context = await server.SendAsync(c => { });
+
+ Assert.Equal("TestValue", context.Response.Headers["TestHeader"]);
+ block.Set();
+ Assert.Equal("BodyStarted,BodyFinished", new StreamReader(context.Response.Body).ReadToEnd());
+ }
+
+ [Fact]
+ public async Task FlushSendsHeaders()
+ {
+ var block = new ManualResetEvent(false);
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(async c =>
+ {
+ c.Response.Headers["TestHeader"] = "TestValue";
+ c.Response.Body.Flush();
+ block.WaitOne();
+ await c.Response.WriteAsync("BodyFinished");
+ });
+ });
+ var server = new TestServer(builder);
+ var context = await server.SendAsync(c => { });
+
+ Assert.Equal("TestValue", context.Response.Headers["TestHeader"]);
+ block.Set();
+ Assert.Equal("BodyFinished", new StreamReader(context.Response.Body).ReadToEnd());
+ }
+
+ [Fact]
+ public async Task ClientDisposalCloses()
+ {
+ var block = new ManualResetEvent(false);
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(async c =>
+ {
+ c.Response.Headers["TestHeader"] = "TestValue";
+ c.Response.Body.Flush();
+ block.WaitOne();
+ await c.Response.WriteAsync("BodyFinished");
+ });
+ });
+ var server = new TestServer(builder);
+ var context = await server.SendAsync(c => { });
+
+ Assert.Equal("TestValue", context.Response.Headers["TestHeader"]);
+ var responseStream = context.Response.Body;
+ Task readTask = responseStream.ReadAsync(new byte[100], 0, 100);
+ Assert.False(readTask.IsCompleted);
+ responseStream.Dispose();
+ Assert.True(readTask.Wait(TimeSpan.FromSeconds(10)));
+ Assert.Equal(0, readTask.Result);
+ block.Set();
+ }
+
+ [Fact]
+ public void ClientCancellationAborts()
+ {
+ var block = new ManualResetEvent(false);
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(c =>
+ {
+ block.Set();
+ Assert.True(c.RequestAborted.WaitHandle.WaitOne(TimeSpan.FromSeconds(10)));
+ c.RequestAborted.ThrowIfCancellationRequested();
+ return Task.CompletedTask;
+ });
+ });
+ var server = new TestServer(builder);
+ var cts = new CancellationTokenSource();
+ var contextTask = server.SendAsync(c => { }, cts.Token);
+ block.WaitOne();
+ cts.Cancel();
+
+ var ex = Assert.Throws(() => contextTask.Wait(TimeSpan.FromSeconds(10)));
+ Assert.IsAssignableFrom(ex.GetBaseException());
+ }
+
+ [Fact]
+ public async Task ClientCancellationAbortsReadAsync()
+ {
+ var block = new ManualResetEvent(false);
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(async c =>
+ {
+ c.Response.Headers["TestHeader"] = "TestValue";
+ c.Response.Body.Flush();
+ block.WaitOne();
+ await c.Response.WriteAsync("BodyFinished");
+ });
+ });
+ var server = new TestServer(builder);
+ var context = await server.SendAsync(c => { });
+
+ Assert.Equal("TestValue", context.Response.Headers["TestHeader"]);
+ var responseStream = context.Response.Body;
+ var cts = new CancellationTokenSource();
+ var readTask = responseStream.ReadAsync(new byte[100], 0, 100, cts.Token);
+ Assert.False(readTask.IsCompleted);
+ cts.Cancel();
+ var ex = Assert.Throws(() => readTask.Wait(TimeSpan.FromSeconds(10)));
+ Assert.IsAssignableFrom(ex.GetBaseException().InnerException);
+ block.Set();
+ }
+
+ [Fact]
+ public Task ExceptionBeforeFirstWriteIsReported()
+ {
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(c =>
+ {
+ throw new InvalidOperationException("Test Exception");
+ });
+ });
+ var server = new TestServer(builder);
+ return Assert.ThrowsAsync(() => server.SendAsync(c => { }));
+ }
+
+ [Fact]
+ public async Task ExceptionAfterFirstWriteIsReported()
+ {
+ var block = new ManualResetEvent(false);
+ var builder = new WebHostBuilder().Configure(app =>
+ {
+ app.Run(async c =>
+ {
+ c.Response.Headers["TestHeader"] = "TestValue";
+ await c.Response.WriteAsync("BodyStarted");
+ block.WaitOne();
+ throw new InvalidOperationException("Test Exception");
+ });
+ });
+ var server = new TestServer(builder);
+ var context = await server.SendAsync(c => { });
+
+ Assert.Equal("TestValue", context.Response.Headers["TestHeader"]);
+ Assert.Equal(11, context.Response.Body.Read(new byte[100], 0, 100));
+ block.Set();
+ var ex = Assert.Throws(() => context.Response.Body.Read(new byte[100], 0, 100));
+ Assert.IsAssignableFrom(ex.InnerException);
+ }
+
+ [Fact]
+ public async Task ClientHandlerCreateContextWithDefaultRequestParameters()
+ {
+ // This logger will attempt to access information from HttpRequest once the HttpContext is created
+ var logger = new VerifierLogger();
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton>(logger);
+ })
+ .Configure(app =>
+ {
+ app.Run(context =>
+ {
+ return Task.FromResult(0);
+ });
+ });
+ var server = new TestServer(builder);
+
+ // The HttpContext will be created and the logger will make sure that the HttpRequest exists and contains reasonable values
+ var ctx = await server.SendAsync(c => { });
+ }
+
+ private class VerifierLogger : ILogger
+ {
+ public IDisposable BeginScope(TState state) => new NoopDispoasble();
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ // This call verifies that fields of HttpRequest are accessed and valid
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) => formatter(state, exception);
+
+ class NoopDispoasble : IDisposable
+ {
+ public void Dispose()
+ {
+ }
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.TestHost.Tests/RequestBuilderTests.cs b/test/Microsoft.AspNetCore.TestHost.Tests/RequestBuilderTests.cs
index b8329ea26f..2a37015aae 100644
--- a/test/Microsoft.AspNetCore.TestHost.Tests/RequestBuilderTests.cs
+++ b/test/Microsoft.AspNetCore.TestHost.Tests/RequestBuilderTests.cs
@@ -2,16 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
namespace Microsoft.AspNetCore.TestHost
{
public class RequestBuilderTests
{
- // c.f. https://github.com/mono/mono/pull/1832
- [ConditionalFact]
- [FrameworkSkipCondition(RuntimeFrameworks.Mono)]
+ [Fact]
public void AddRequestHeader()
{
var builder = new WebHostBuilder().Configure(app => { });
diff --git a/test/Microsoft.AspNetCore.TestHost.Tests/TestClientTests.cs b/test/Microsoft.AspNetCore.TestHost.Tests/TestClientTests.cs
index 748e79c841..0bb8880823 100644
--- a/test/Microsoft.AspNetCore.TestHost.Tests/TestClientTests.cs
+++ b/test/Microsoft.AspNetCore.TestHost.Tests/TestClientTests.cs
@@ -267,14 +267,8 @@ namespace Microsoft.AspNetCore.TestHost
}
};
var builder = new WebHostBuilder()
- .ConfigureServices(services =>
- {
- services.AddSingleton>(logger);
- })
- .Configure(app =>
- {
- app.Run(appDelegate);
- });
+ .ConfigureServices(services => services.AddSingleton>(logger))
+ .Configure(app => app.Run(appDelegate));
var server = new TestServer(builder);
// Act
@@ -283,7 +277,7 @@ namespace Microsoft.AspNetCore.TestHost
tokenSource.Cancel();
// Assert
- await Assert.ThrowsAnyAsync(async () => await client.ConnectAsync(new System.Uri("http://localhost"), tokenSource.Token));
+ await Assert.ThrowsAnyAsync(async () => await client.ConnectAsync(new Uri("http://localhost"), tokenSource.Token));
}
private class VerifierLogger : ILogger