#816 Allow directly constructing an HttpContext for TestServer

This commit is contained in:
Chris Ross (ASP.NET) 2017-10-27 12:05:52 -07:00
parent c5f2333481
commit 82ccf4f06e
8 changed files with 562 additions and 311 deletions

View File

@ -33,12 +33,7 @@ namespace Microsoft.AspNetCore.TestHost
/// <param name="application">The <see cref="IHttpApplication{TContext}"/>.</param>
public ClientHandler(PathString pathBase, IHttpApplication<Context> 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<Context> _application;
private TaskCompletionSource<HttpResponseMessage> _responseTcs;
private ResponseStream _responseStream;
private ResponseFeature _responseFeature;
private CancellationTokenSource _requestAbortedSource;
private bool _pipelineFinished;
internal RequestState(HttpRequestMessage request, PathString pathBase, IHttpApplication<Context> application)
{
_request = request;
_application = application;
_responseTcs = new TaskCompletionSource<HttpResponseMessage>();
_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<IHttpRequestFeature>(requestFeature);
_responseFeature = new ResponseFeature();
contextFeatures.Set<IHttpResponseFeature>(_responseFeature);
var requestLifetimeFeature = new HttpRequestLifetimeFeature();
contextFeatures.Set<IHttpRequestLifetimeFeature>(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<HttpResponseMessage> 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<IHttpResponseFeature>().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<string>)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<string>)header.Value);
Contract.Assert(success, "Bad header");
}
}
private async Task<HttpResponseMessage> GenerateResponseAsync()
{
await _responseFeature.FireOnSendingHeadersAsync();
var httpContext = Context.HttpContext;
var response = new HttpResponseMessage();
response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
response.ReasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().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<string>)header.Value))
{
bool success = response.Content.Headers.TryAddWithoutValidation(header.Key, (IEnumerable<string>)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;
}
}
}

View File

@ -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<Context> _application;
private readonly HttpContext _httpContext;
private TaskCompletionSource<HttpContext> _responseTcs = new TaskCompletionSource<HttpContext>(TaskCreationOptions.RunContinuationsAsynchronously);
private ResponseStream _responseStream;
private ResponseFeature _responseFeature = new ResponseFeature();
private CancellationTokenSource _requestAbortedSource = new CancellationTokenSource();
private bool _pipelineFinished;
private Context _testContext;
internal HttpContextBuilder(IHttpApplication<Context> 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<IHttpResponseFeature>(_responseFeature);
var requestLifetimeFeature = new HttpRequestLifetimeFeature();
requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token;
_httpContext.Features.Set<IHttpRequestLifetimeFeature>(requestLifetimeFeature);
_responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest);
_responseFeature.Body = _responseStream;
}
internal void Configure(Action<HttpContext> configureContext)
{
if (configureContext == null)
{
throw new ArgumentNullException(nameof(configureContext));
}
configureContext(_httpContext);
}
/// <summary>
/// Start processing the request.
/// </summary>
/// <returns></returns>
internal Task<HttpContext> 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);
}
}
}

View File

@ -82,6 +82,38 @@ namespace Microsoft.AspNetCore.TestHost
return new RequestBuilder(this, path);
}
/// <summary>
/// Creates, configures, sends, and returns a <see cref="HttpContext"/>. This completes as soon as the response is started.
/// </summary>
/// <returns></returns>
public async Task<HttpContext> SendAsync(Action<HttpContext> 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)

View File

@ -22,12 +22,7 @@ namespace Microsoft.AspNetCore.TestHost
internal WebSocketClient(PathString pathBase, IHttpApplication<Context> 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<WebSocket> 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<Context> _application;
private TaskCompletionSource<WebSocket> _clientWebSocketTcs;
private CancellationTokenRegistration _cancellationTokenRegistration;
private WebSocket _serverWebSocket;
public Context Context { get; private set; }
public Task<WebSocket> WebSocketTask { get { return _clientWebSocketTcs.Task; } }
public RequestState(Uri uri, PathString pathBase, CancellationToken cancellationToken, IHttpApplication<Context> application)
{
_clientWebSocketTcs = new TaskCompletionSource<WebSocket>();
_cancellationTokenRegistration = cancellationToken.Register(
() => _clientWebSocketTcs.TrySetCanceled(cancellationToken));
_application = application;
// HttpContext
var contextFeatures = new FeatureCollection();
contextFeatures.Set<IHttpRequestFeature>(new RequestFeature());
contextFeatures.Set<IHttpResponseFeature>(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<IHttpWebSocketFeature>(this);
}
webSocketFeature = new WebSocketFeature(context);
context.Features.Set<IHttpWebSocketFeature>(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<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
async Task<WebSocket> 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;
}
}
}

View File

@ -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

View File

@ -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<IHttpResponseFeature>().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<int> 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<AggregateException>(() => contextTask.Wait(TimeSpan.FromSeconds(10)));
Assert.IsAssignableFrom<OperationCanceledException>(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<AggregateException>(() => readTask.Wait(TimeSpan.FromSeconds(10)));
Assert.IsAssignableFrom<OperationCanceledException>(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<InvalidOperationException>(() => 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<IOException>(() => context.Response.Body.Read(new byte[100], 0, 100));
Assert.IsAssignableFrom<InvalidOperationException>(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<ILogger<IWebHost>>(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<IWebHost>
{
public IDisposable BeginScope<TState>(TState state) => new NoopDispoasble();
public bool IsEnabled(LogLevel logLevel) => true;
// This call verifies that fields of HttpRequest are accessed and valid
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) => formatter(state, exception);
class NoopDispoasble : IDisposable
{
public void Dispose()
{
}
}
}
}
}

View File

@ -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 => { });

View File

@ -267,14 +267,8 @@ namespace Microsoft.AspNetCore.TestHost
}
};
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<ILogger<IWebHost>>(logger);
})
.Configure(app =>
{
app.Run(appDelegate);
});
.ConfigureServices(services => services.AddSingleton<ILogger<IWebHost>>(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<OperationCanceledException>(async () => await client.ConnectAsync(new System.Uri("http://localhost"), tokenSource.Token));
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await client.ConnectAsync(new Uri("http://localhost"), tokenSource.Token));
}
private class VerifierLogger : ILogger<IWebHost>