#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>
|
/// <param name="application">The <see cref="IHttpApplication{TContext}"/>.</param>
|
||||||
public ClientHandler(PathString pathBase, IHttpApplication<Context> application)
|
public ClientHandler(PathString pathBase, IHttpApplication<Context> application)
|
||||||
{
|
{
|
||||||
if (application == null)
|
_application = application ?? throw new ArgumentNullException(nameof(application));
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(application));
|
|
||||||
}
|
|
||||||
|
|
||||||
_application = application;
|
|
||||||
|
|
||||||
// PathString.StartsWithSegments that we use below requires the base path to not end in a slash.
|
// PathString.StartsWithSegments that we use below requires the base path to not end in a slash.
|
||||||
if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
|
if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
|
||||||
|
|
@ -64,187 +59,74 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
throw new ArgumentNullException(nameof(request));
|
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 requestContent = request.Content ?? new StreamContent(Stream.Null);
|
||||||
var body = await requestContent.ReadAsStreamAsync();
|
var body = await requestContent.ReadAsStreamAsync();
|
||||||
if (body.CanSeek)
|
contextBuilder.Configure(context =>
|
||||||
{
|
{
|
||||||
// This body may have been consumed before, rewind it.
|
var req = context.Request;
|
||||||
body.Seek(0, SeekOrigin.Begin);
|
|
||||||
}
|
|
||||||
state.Context.HttpContext.Request.Body = body;
|
|
||||||
var registration = cancellationToken.Register(state.AbortRequest);
|
|
||||||
|
|
||||||
// Async offload, don't let the test code block the caller.
|
req.Protocol = "HTTP/" + request.Version.ToString(fieldCount: 2);
|
||||||
var offload = Task.Factory.StartNew(async () =>
|
req.Method = request.Method.ToString();
|
||||||
{
|
|
||||||
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.Scheme = request.RequestUri.Scheme;
|
||||||
|
req.Host = HostString.FromUriComponent(request.RequestUri);
|
||||||
if (request.RequestUri.IsDefaultPort)
|
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;
|
||||||
}
|
}
|
||||||
|
req.QueryString = QueryString.FromUriComponent(request.RequestUri);
|
||||||
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;
|
|
||||||
|
|
||||||
foreach (var header in request.Headers)
|
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)
|
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);
|
if (body.CanSeek)
|
||||||
_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)
|
|
||||||
{
|
{
|
||||||
_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()
|
responseBody = context.Response.Body;
|
||||||
{
|
});
|
||||||
_pipelineFinished = true;
|
|
||||||
await ReturnResponseMessageAsync();
|
|
||||||
_responseStream.Complete();
|
|
||||||
await _responseFeature.FireOnResponseCompletedAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
if (!response.Headers.TryAddWithoutValidation(header.Key, (IEnumerable<string>)header.Value))
|
||||||
// (as it happens on a different thread) by which point the CompleteResponseAsync could run and calls this
|
|
||||||
// method again.
|
|
||||||
if (!Context.HttpContext.Response.HasStarted)
|
|
||||||
{
|
{
|
||||||
var response = await GenerateResponseAsync();
|
bool success = response.Content.Headers.TryAddWithoutValidation(header.Key, (IEnumerable<string>)header.Value);
|
||||||
// Dispatch, as TrySetResult will synchronously execute the waiters callback and block our Write.
|
Contract.Assert(success, "Bad header");
|
||||||
var setResult = Task.Factory.StartNew(() => _responseTcs.TrySetResult(response));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return response;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,7 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
|
|
||||||
internal WebSocketClient(PathString pathBase, IHttpApplication<Context> application)
|
internal WebSocketClient(PathString pathBase, IHttpApplication<Context> application)
|
||||||
{
|
{
|
||||||
if (application == null)
|
_application = application ?? throw new ArgumentNullException(nameof(application));
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(application));
|
|
||||||
}
|
|
||||||
|
|
||||||
_application = application;
|
|
||||||
|
|
||||||
// PathString.StartsWithSegments that we use below requires the base path to not end in a slash.
|
// PathString.StartsWithSegments that we use below requires the base path to not end in a slash.
|
||||||
if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
|
if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
|
||||||
|
|
@ -53,79 +48,21 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
|
|
||||||
public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken)
|
public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var state = new RequestState(uri, _pathBase, cancellationToken, _application);
|
WebSocketFeature webSocketFeature = null;
|
||||||
|
var contextBuilder = new HttpContextBuilder(_application);
|
||||||
if (ConfigureRequest != null)
|
contextBuilder.Configure(context =>
|
||||||
{
|
{
|
||||||
ConfigureRequest(state.Context.HttpContext.Request);
|
var request = 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);
|
|
||||||
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 scheme = uri.Scheme;
|
var scheme = uri.Scheme;
|
||||||
scheme = (scheme == "ws") ? "http" : scheme;
|
scheme = (scheme == "ws") ? "http" : scheme;
|
||||||
scheme = (scheme == "wss") ? "https" : scheme;
|
scheme = (scheme == "wss") ? "https" : scheme;
|
||||||
request.Scheme = scheme;
|
request.Scheme = scheme;
|
||||||
request.Method = "GET";
|
request.Path = PathString.FromUriComponent(uri);
|
||||||
var fullPath = PathString.FromUriComponent(uri);
|
request.PathBase = PathString.Empty;
|
||||||
PathString remainder;
|
if (request.Path.StartsWithSegments(_pathBase, out var remainder))
|
||||||
if (fullPath.StartsWithSegments(pathBase, out remainder))
|
|
||||||
{
|
{
|
||||||
request.PathBase = pathBase;
|
|
||||||
request.Path = remainder;
|
request.Path = remainder;
|
||||||
}
|
request.PathBase = _pathBase;
|
||||||
else
|
|
||||||
{
|
|
||||||
request.PathBase = PathString.Empty;
|
|
||||||
request.Path = fullPath;
|
|
||||||
}
|
}
|
||||||
request.QueryString = QueryString.FromUriComponent(uri);
|
request.QueryString = QueryString.FromUriComponent(uri);
|
||||||
request.Headers.Add("Connection", new string[] { "Upgrade" });
|
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.Headers.Add("Sec-WebSocket-Key", new string[] { CreateRequestKey() });
|
||||||
request.Body = Stream.Null;
|
request.Body = Stream.Null;
|
||||||
|
|
||||||
// Response
|
|
||||||
var response = httpContext.Response;
|
|
||||||
response.Body = Stream.Null;
|
|
||||||
response.StatusCode = 200;
|
|
||||||
|
|
||||||
// WebSocket
|
// 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);
|
||||||
}
|
}
|
||||||
|
if (webSocketFeature.ClientWebSocket == null)
|
||||||
public void PipelineFailed(Exception ex)
|
|
||||||
{
|
{
|
||||||
_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)
|
_httpContext = context;
|
||||||
{
|
|
||||||
_serverWebSocket.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ServerCleanup(Exception exception)
|
bool IHttpWebSocketFeature.IsWebSocketRequest => true;
|
||||||
{
|
|
||||||
_application.DisposeContext(Context, exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CreateRequestKey()
|
public WebSocket ClientWebSocket { get; private set; }
|
||||||
{
|
|
||||||
byte[] data = new byte[16];
|
|
||||||
var rng = RandomNumberGenerator.Create();
|
|
||||||
rng.GetBytes(data);
|
|
||||||
return Convert.ToBase64String(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IHttpWebSocketFeature.IsWebSocketRequest
|
public WebSocket ServerWebSocket { get; private set; }
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
|
async Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
|
||||||
{
|
{
|
||||||
var websockets = TestWebSocket.CreatePair(context.SubProtocol);
|
var websockets = TestWebSocket.CreatePair(context.SubProtocol);
|
||||||
if (_clientWebSocketTcs.TrySetResult(websockets.Item1))
|
if (_httpContext.Response.HasStarted)
|
||||||
{
|
{
|
||||||
Context.HttpContext.Response.StatusCode = StatusCodes.Status101SwitchingProtocols;
|
throw new InvalidOperationException("The response has already started");
|
||||||
_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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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/");
|
return httpClient.GetAsync("https://example.com/");
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task ResubmitRequestWorks()
|
public async Task ResubmitRequestWorks()
|
||||||
{
|
{
|
||||||
int requestCount = 1;
|
int requestCount = 1;
|
||||||
|
|
@ -112,8 +111,7 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
Assert.Equal("TestValue:2", response.Headers.GetValues("TestHeader").First());
|
Assert.Equal("TestValue:2", response.Headers.GetValues("TestHeader").First());
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task MiddlewareOnlySetsHeaders()
|
public async Task MiddlewareOnlySetsHeaders()
|
||||||
{
|
{
|
||||||
var handler = new ClientHandler(PathString.Empty, new DummyApplication(context =>
|
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());
|
Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First());
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task BlockingMiddlewareShouldNotBlockClient()
|
public async Task BlockingMiddlewareShouldNotBlockClient()
|
||||||
{
|
{
|
||||||
ManualResetEvent block = new ManualResetEvent(false);
|
ManualResetEvent block = new ManualResetEvent(false);
|
||||||
|
|
@ -144,8 +141,7 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
HttpResponseMessage response = await task;
|
HttpResponseMessage response = await task;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task HeadersAvailableBeforeBodyFinished()
|
public async Task HeadersAvailableBeforeBodyFinished()
|
||||||
{
|
{
|
||||||
ManualResetEvent block = new ManualResetEvent(false);
|
ManualResetEvent block = new ManualResetEvent(false);
|
||||||
|
|
@ -164,8 +160,7 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
Assert.Equal("BodyStarted,BodyFinished", await response.Content.ReadAsStringAsync());
|
Assert.Equal("BodyStarted,BodyFinished", await response.Content.ReadAsStringAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task FlushSendsHeaders()
|
public async Task FlushSendsHeaders()
|
||||||
{
|
{
|
||||||
ManualResetEvent block = new ManualResetEvent(false);
|
ManualResetEvent block = new ManualResetEvent(false);
|
||||||
|
|
@ -184,8 +179,7 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
Assert.Equal("BodyFinished", await response.Content.ReadAsStringAsync());
|
Assert.Equal("BodyFinished", await response.Content.ReadAsStringAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task ClientDisposalCloses()
|
public async Task ClientDisposalCloses()
|
||||||
{
|
{
|
||||||
ManualResetEvent block = new ManualResetEvent(false);
|
ManualResetEvent block = new ManualResetEvent(false);
|
||||||
|
|
@ -210,8 +204,7 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
block.Set();
|
block.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task ClientCancellationAborts()
|
public async Task ClientCancellationAborts()
|
||||||
{
|
{
|
||||||
ManualResetEvent block = new ManualResetEvent(false);
|
ManualResetEvent block = new ManualResetEvent(false);
|
||||||
|
|
@ -249,8 +242,7 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
HttpCompletionOption.ResponseHeadersRead));
|
HttpCompletionOption.ResponseHeadersRead));
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalFact]
|
[Fact]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task ExceptionAfterFirstWriteIsReported()
|
public async Task ExceptionAfterFirstWriteIsReported()
|
||||||
{
|
{
|
||||||
ManualResetEvent block = new ManualResetEvent(false);
|
ManualResetEvent block = new ManualResetEvent(false);
|
||||||
|
|
@ -326,10 +318,8 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
[ConditionalFact]
|
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")]
|
|
||||||
public async Task ClientHandlerCreateContextWithDefaultRequestParameters()
|
public async Task ClientHandlerCreateContextWithDefaultRequestParameters()
|
||||||
{
|
{
|
||||||
// This logger will attempt to access information from HttpRequest once the HttpContext is created
|
// 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.
|
// 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.Hosting;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.TestHost
|
namespace Microsoft.AspNetCore.TestHost
|
||||||
{
|
{
|
||||||
public class RequestBuilderTests
|
public class RequestBuilderTests
|
||||||
{
|
{
|
||||||
// c.f. https://github.com/mono/mono/pull/1832
|
[Fact]
|
||||||
[ConditionalFact]
|
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
|
||||||
public void AddRequestHeader()
|
public void AddRequestHeader()
|
||||||
{
|
{
|
||||||
var builder = new WebHostBuilder().Configure(app => { });
|
var builder = new WebHostBuilder().Configure(app => { });
|
||||||
|
|
|
||||||
|
|
@ -267,14 +267,8 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var builder = new WebHostBuilder()
|
var builder = new WebHostBuilder()
|
||||||
.ConfigureServices(services =>
|
.ConfigureServices(services => services.AddSingleton<ILogger<IWebHost>>(logger))
|
||||||
{
|
.Configure(app => app.Run(appDelegate));
|
||||||
services.AddSingleton<ILogger<IWebHost>>(logger);
|
|
||||||
})
|
|
||||||
.Configure(app =>
|
|
||||||
{
|
|
||||||
app.Run(appDelegate);
|
|
||||||
});
|
|
||||||
var server = new TestServer(builder);
|
var server = new TestServer(builder);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -283,7 +277,7 @@ namespace Microsoft.AspNetCore.TestHost
|
||||||
tokenSource.Cancel();
|
tokenSource.Cancel();
|
||||||
|
|
||||||
// Assert
|
// 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>
|
private class VerifierLogger : ILogger<IWebHost>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue