#816 Allow directly constructing an HttpContext for TestServer
This commit is contained in:
parent
c5f2333481
commit
82ccf4f06e
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 => { });
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue