From ed38d28db44460d172376f003797eccd7df14f72 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 31 Jul 2014 11:37:27 -0700 Subject: [PATCH] #65 - Port more TestHost funcationality. --- Hosting.sln | 7 +- .../ClientHandler.cs | 207 ++++++++++ .../Microsoft.AspNet.TestHost.kproj | 9 +- .../RequestBuilder.cs | 111 +++++ .../RequestFeature.cs | 41 ++ .../RequestInformation.cs | 51 --- .../ResponseFeature.cs | 48 +++ .../ResponseInformation.cs | 50 --- .../ResponseStream.cs | 380 ++++++++++++++++++ src/Microsoft.AspNet.TestHost/TestClient.cs | 22 +- src/Microsoft.AspNet.TestHost/TestServer.cs | 89 ++-- src/Microsoft.AspNet.TestHost/project.json | 3 +- .../ClientHandlerTests.cs | 231 +++++++++++ .../Microsoft.AspNet.TestHost.Tests.kproj | 6 +- .../RequestBuilderTests.cs | 35 ++ .../ResponseFeatureTests.cs | 20 + .../ResponseInformationTests.cs | 34 -- .../TestApplicationEnvironment.cs | 20 +- .../TestClientTests.cs | 20 +- .../TestServerTests.cs | 107 +++-- 20 files changed, 1223 insertions(+), 268 deletions(-) create mode 100644 src/Microsoft.AspNet.TestHost/ClientHandler.cs create mode 100644 src/Microsoft.AspNet.TestHost/RequestBuilder.cs create mode 100644 src/Microsoft.AspNet.TestHost/RequestFeature.cs delete mode 100644 src/Microsoft.AspNet.TestHost/RequestInformation.cs create mode 100644 src/Microsoft.AspNet.TestHost/ResponseFeature.cs delete mode 100644 src/Microsoft.AspNet.TestHost/ResponseInformation.cs create mode 100644 src/Microsoft.AspNet.TestHost/ResponseStream.cs create mode 100644 test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs create mode 100644 test/Microsoft.AspNet.TestHost.Tests/RequestBuilderTests.cs create mode 100644 test/Microsoft.AspNet.TestHost.Tests/ResponseFeatureTests.cs delete mode 100644 test/Microsoft.AspNet.TestHost.Tests/ResponseInformationTests.cs diff --git a/Hosting.sln b/Hosting.sln index a1be8041c3..52888aa65a 100644 --- a/Hosting.sln +++ b/Hosting.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.21730.1 +VisualStudioVersion = 14.0.21916.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E0497F39-AFFB-4819-A116-E39E361915AB}" EndProject @@ -17,6 +17,11 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Hosting.Te EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.RequestContainer", "src\Microsoft.AspNet.RequestContainer\Microsoft.AspNet.RequestContainer.kproj", "{374A5B0C-3E93-4A23-A4A0-EE2AB6DF7814}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A66E3673-3976-4152-B902-2D0EC1428EA2}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/Microsoft.AspNet.TestHost/ClientHandler.cs b/src/Microsoft.AspNet.TestHost/ClientHandler.cs new file mode 100644 index 0000000000..d92eaffdda --- /dev/null +++ b/src/Microsoft.AspNet.TestHost/ClientHandler.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.FeatureModel; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.PipelineCore; + +namespace Microsoft.AspNet.TestHost +{ + /// + /// This adapts HttpRequestMessages to ASP.NET requests, dispatches them through the pipeline, and returns the + /// associated HttpResponseMessage. + /// + public class ClientHandler : HttpMessageHandler + { + private readonly Func _next; + + /// + /// Create a new handler. + /// + /// The pipeline entry point. + public ClientHandler(Func next) + { + if (next == null) + { + throw new ArgumentNullException("next"); + } + + _next = next; + } + + /// + /// This adapts HttpRequestMessages to ASP.NET requests, dispatches them through the pipeline, and returns the + /// associated HttpResponseMessage. + /// + /// + /// + /// + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + var state = new RequestState(request, cancellationToken); + HttpContent requestContent = request.Content ?? new StreamContent(Stream.Null); + Stream body = await requestContent.ReadAsStreamAsync(); + if (body.CanSeek) + { + // This body may have been consumed before, rewind it. + body.Seek(0, SeekOrigin.Begin); + } + state.HttpContext.Request.Body = body; + CancellationTokenRegistration registration = cancellationToken.Register(state.Abort); + + // Async offload, don't let the test code block the caller. + Task offload = Task.Factory.StartNew(async () => + { + try + { + await _next(state.FeatureCollection); + state.CompleteResponse(); + } + catch (Exception ex) + { + state.Abort(ex); + } + finally + { + registration.Dispose(); + state.Dispose(); + } + }); + + return await state.ResponseTask; + } + + private class RequestState : IDisposable + { + private readonly HttpRequestMessage _request; + private TaskCompletionSource _responseTcs; + private ResponseStream _responseStream; + private ResponseFeature _responseFeature; + + internal RequestState(HttpRequestMessage request, CancellationToken cancellationToken) + { + _request = request; + _responseTcs = new TaskCompletionSource(); + + if (request.RequestUri.IsDefaultPort) + { + request.Headers.Host = request.RequestUri.Host; + } + else + { + request.Headers.Host = request.RequestUri.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped); + } + + FeatureCollection = new FeatureCollection(); + HttpContext = new DefaultHttpContext(FeatureCollection); + HttpContext.SetFeature(new RequestFeature()); + _responseFeature = new ResponseFeature(); + HttpContext.SetFeature(_responseFeature); + var serverRequest = HttpContext.Request; + serverRequest.Protocol = "HTTP/" + request.Version.ToString(2); + serverRequest.Scheme = request.RequestUri.Scheme; + serverRequest.Method = request.Method.ToString(); + serverRequest.Path = PathString.FromUriComponent(request.RequestUri); + serverRequest.PathBase = PathString.Empty; + serverRequest.QueryString = QueryString.FromUriComponent(request.RequestUri); + // TODO: serverRequest.CallCancelled = cancellationToken; + + foreach (var header in request.Headers) + { + serverRequest.Headers.AppendValues(header.Key, header.Value.ToArray()); + } + HttpContent requestContent = request.Content; + if (requestContent != null) + { + foreach (var header in request.Content.Headers) + { + serverRequest.Headers.AppendValues(header.Key, header.Value.ToArray()); + } + } + + _responseStream = new ResponseStream(CompleteResponse); + HttpContext.Response.Body = _responseStream; + HttpContext.Response.StatusCode = 200; + } + + public HttpContext HttpContext { get; private set; } + + public IFeatureCollection FeatureCollection { get; private set; } + + public Task ResponseTask + { + get { return _responseTcs.Task; } + } + + internal void CompleteResponse() + { + if (!_responseTcs.Task.IsCompleted) + { + HttpResponseMessage response = GenerateResponse(); + // Dispatch, as TrySetResult will synchronously execute the waiters callback and block our Write. + Task.Factory.StartNew(() => _responseTcs.TrySetResult(response)); + } + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", + Justification = "HttpResposneMessage must be returned to the caller.")] + internal HttpResponseMessage GenerateResponse() + { + _responseFeature.FireOnSendingHeaders(); + + var response = new HttpResponseMessage(); + response.StatusCode = (HttpStatusCode)HttpContext.Response.StatusCode; + response.ReasonPhrase = HttpContext.GetFeature().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, header.Value)) + { + bool success = response.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + Contract.Assert(success, "Bad header"); + } + } + return response; + } + + internal void Abort() + { + Abort(new OperationCanceledException()); + } + + internal void Abort(Exception exception) + { + _responseStream.Abort(exception); + _responseTcs.TrySetException(exception); + } + + public void Dispose() + { + _responseStream.Dispose(); + // Do not dispose the request, that will be disposed by the caller. + } + } + } +} diff --git a/src/Microsoft.AspNet.TestHost/Microsoft.AspNet.TestHost.kproj b/src/Microsoft.AspNet.TestHost/Microsoft.AspNet.TestHost.kproj index 882610a036..4bba9328cc 100644 --- a/src/Microsoft.AspNet.TestHost/Microsoft.AspNet.TestHost.kproj +++ b/src/Microsoft.AspNet.TestHost/Microsoft.AspNet.TestHost.kproj @@ -20,11 +20,14 @@ + - - + + + + - + \ No newline at end of file diff --git a/src/Microsoft.AspNet.TestHost/RequestBuilder.cs b/src/Microsoft.AspNet.TestHost/RequestBuilder.cs new file mode 100644 index 0000000000..57422b4b33 --- /dev/null +++ b/src/Microsoft.AspNet.TestHost/RequestBuilder.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.TestHost +{ + /// + /// Used to construct a HttpRequestMessage object. + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", + Justification = "HttpRequestMessage is disposed by HttpClient in SendAsync")] + public class RequestBuilder + { + private readonly TestServer _server; + private readonly HttpRequestMessage _req; + + /// + /// Construct a new HttpRequestMessage with the given path. + /// + /// + /// + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Not a full URI")] + public RequestBuilder(TestServer server, string path) + { + if (server == null) + { + throw new ArgumentNullException("server"); + } + + _server = server; + _req = new HttpRequestMessage(HttpMethod.Get, path); + } + + /// + /// Configure any HttpRequestMessage properties. + /// + /// + /// + public RequestBuilder And(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException("configure"); + } + + configure(_req); + return this; + } + + /// + /// Add the given header and value to the request or request content. + /// + /// + /// + /// + public RequestBuilder AddHeader(string name, string value) + { + if (!_req.Headers.TryAddWithoutValidation(name, value)) + { + if (_req.Content == null) + { + _req.Content = new StreamContent(Stream.Null); + } + if (!_req.Content.Headers.TryAddWithoutValidation(name, value)) + { + // TODO: throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidHeaderName, name), "name"); + throw new ArgumentException("Invalid header name: " + name, "name"); + } + } + return this; + } + + /// + /// Set the request method and start processing the request. + /// + /// + /// + public Task SendAsync(string method) + { + _req.Method = new HttpMethod(method); + return _server.CreateClient().SendAsync(_req); + } + + /// + /// Set the request method to GET and start processing the request. + /// + /// + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "GET is an HTTP verb.")] + public Task GetAsync() + { + _req.Method = HttpMethod.Get; + return _server.CreateClient().SendAsync(_req); + } + + /// + /// Set the request method to POST and start processing the request. + /// + /// + public Task PostAsync() + { + _req.Method = HttpMethod.Post; + return _server.CreateClient().SendAsync(_req); + } + } +} diff --git a/src/Microsoft.AspNet.TestHost/RequestFeature.cs b/src/Microsoft.AspNet.TestHost/RequestFeature.cs new file mode 100644 index 0000000000..2ea5ae5127 --- /dev/null +++ b/src/Microsoft.AspNet.TestHost/RequestFeature.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using System.IO; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.AspNet.TestHost +{ + internal class RequestFeature : IHttpRequestFeature + { + public RequestFeature() + { + Body = Stream.Null; + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + Method = "GET"; + Path = ""; + PathBase = ""; + Protocol = "HTTP/1.1"; + QueryString = ""; + Scheme = "http"; + } + + public Stream Body { get; set; } + + public IDictionary Headers { get; set; } + + public string Method { get; set; } + + public string Path { get; set; } + + public string PathBase { get; set; } + + public string Protocol { get; set; } + + public string QueryString { get; set; } + + public string Scheme { get; set; } + } +} diff --git a/src/Microsoft.AspNet.TestHost/RequestInformation.cs b/src/Microsoft.AspNet.TestHost/RequestInformation.cs deleted file mode 100644 index d5815ee993..0000000000 --- a/src/Microsoft.AspNet.TestHost/RequestInformation.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNet.HttpFeature; - -namespace Microsoft.AspNet.TestHost -{ - internal class RequestInformation : IHttpRequestFeature - { - public RequestInformation() - { - Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); - PathBase = ""; - Body = Stream.Null; - Protocol = "HTTP/1.1"; - } - - public Stream Body { get; set; } - - public IDictionary Headers { get; set; } - - public string Method { get; set; } - - public string Path { get; set; } - - public string PathBase { get; set; } - - public string Protocol { get; set; } - - public string QueryString { get; set; } - - public string Scheme { get; set; } - } -} diff --git a/src/Microsoft.AspNet.TestHost/ResponseFeature.cs b/src/Microsoft.AspNet.TestHost/ResponseFeature.cs new file mode 100644 index 0000000000..5b7d230a13 --- /dev/null +++ b/src/Microsoft.AspNet.TestHost/ResponseFeature.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using System.IO; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.AspNet.TestHost +{ + internal class ResponseFeature : IHttpResponseFeature + { + private Action _sendingHeaders = () => { }; + + public ResponseFeature() + { + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + Body = new MemoryStream(); + + // 200 is the default status code all the way down to the host, so we set it + // here to be consistent with the rest of the hosts when writing tests. + StatusCode = 200; + } + + public int StatusCode { get; set; } + + public string ReasonPhrase { get; set; } + + public IDictionary Headers { get; set; } + + public Stream Body { get; set; } + + public void OnSendingHeaders(Action callback, object state) + { + var prior = _sendingHeaders; + _sendingHeaders = () => + { + callback(state); + prior(); + }; + } + + public void FireOnSendingHeaders() + { + _sendingHeaders(); + } + } +} diff --git a/src/Microsoft.AspNet.TestHost/ResponseInformation.cs b/src/Microsoft.AspNet.TestHost/ResponseInformation.cs deleted file mode 100644 index 7cccbafc53..0000000000 --- a/src/Microsoft.AspNet.TestHost/ResponseInformation.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNet.HttpFeature; - -namespace Microsoft.AspNet.TestHost -{ - internal class ResponseInformation : IHttpResponseFeature - { - public ResponseInformation() - { - Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); - Body = new MemoryStream(); - - // 200 is the default status code all the way down to the host, so we set it - // here to be consistent with the rest of the hosts when writing tests. - StatusCode = 200; - } - - public int StatusCode { get; set; } - - public string ReasonPhrase { get; set; } - - public IDictionary Headers { get; set; } - - public Stream Body { get; set; } - - public void OnSendingHeaders(Action callback, object state) - { - // TODO: Figure out how to implement this thing. - } - } -} diff --git a/src/Microsoft.AspNet.TestHost/ResponseStream.cs b/src/Microsoft.AspNet.TestHost/ResponseStream.cs new file mode 100644 index 0000000000..cae4eba46f --- /dev/null +++ b/src/Microsoft.AspNet.TestHost/ResponseStream.cs @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.TestHost +{ + // This steam accepts writes from the server/app, buffers them internally, and returns the data via Reads + // when requested by the client. + internal class ResponseStream : Stream + { + private bool _disposed; + private bool _aborted; + private Exception _abortException; + private ConcurrentQueue _bufferedData; + private ArraySegment _topBuffer; + private SemaphoreSlim _readLock; + private SemaphoreSlim _writeLock; + private TaskCompletionSource _readWaitingForData; + private object _signalReadLock; + + private Action _onFirstWrite; + private bool _firstWrite; + + internal ResponseStream(Action onFirstWrite) + { + if (onFirstWrite == null) + { + throw new ArgumentNullException("onFirstWrite"); + } + _onFirstWrite = onFirstWrite; + _firstWrite = true; + + _readLock = new SemaphoreSlim(1, 1); + _writeLock = new SemaphoreSlim(1, 1); + _bufferedData = new ConcurrentQueue(); + _readWaitingForData = new TaskCompletionSource(); + _signalReadLock = new object(); + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + #region NotSupported + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + #endregion NotSupported + + public override void Flush() + { + CheckDisposed(); + + _writeLock.Wait(); + try + { + FirstWrite(); + } + finally + { + _writeLock.Release(); + } + + // TODO: Wait for data to drain? + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetCanceled(); + return tcs.Task; + } + + Flush(); + + // TODO: Wait for data to drain? + + return Task.FromResult(null); + } + + public override int Read(byte[] buffer, int offset, int count) + { + VerifyBuffer(buffer, offset, count, allowEmpty: false); + _readLock.Wait(); + try + { + int totalRead = 0; + do + { + // Don't drain buffered data when signaling an abort. + CheckAborted(); + if (_topBuffer.Count <= 0) + { + byte[] topBuffer = null; + while (!_bufferedData.TryDequeue(out topBuffer)) + { + if (_disposed) + { + CheckAborted(); + // Graceful close + return totalRead; + } + WaitForDataAsync().Wait(); + } + _topBuffer = new ArraySegment(topBuffer); + } + int actualCount = Math.Min(count, _topBuffer.Count); + Buffer.BlockCopy(_topBuffer.Array, _topBuffer.Offset, buffer, offset, actualCount); + _topBuffer = new ArraySegment(_topBuffer.Array, + _topBuffer.Offset + actualCount, + _topBuffer.Count - actualCount); + totalRead += actualCount; + offset += actualCount; + count -= actualCount; + } + while (count > 0 && (_topBuffer.Count > 0 || _bufferedData.Count > 0)); + // Keep reading while there is more data available and we have more space to put it in. + return totalRead; + } + finally + { + _readLock.Release(); + } + } +#if NET45 + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + // TODO: This option doesn't preserve the state object. + // return ReadAsync(buffer, offset, count); + return base.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + // return ((Task)asyncResult).Result; + return base.EndRead(asyncResult); + } +#endif + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + VerifyBuffer(buffer, offset, count, allowEmpty: false); + CancellationTokenRegistration registration = cancellationToken.Register(Abort); + await _readLock.WaitAsync(cancellationToken); + try + { + int totalRead = 0; + do + { + // Don't drained buffered data on abort. + CheckAborted(); + if (_topBuffer.Count <= 0) + { + byte[] topBuffer = null; + while (!_bufferedData.TryDequeue(out topBuffer)) + { + if (_disposed) + { + CheckAborted(); + // Graceful close + return totalRead; + } + await WaitForDataAsync(); + } + _topBuffer = new ArraySegment(topBuffer); + } + int actualCount = Math.Min(count, _topBuffer.Count); + Buffer.BlockCopy(_topBuffer.Array, _topBuffer.Offset, buffer, offset, actualCount); + _topBuffer = new ArraySegment(_topBuffer.Array, + _topBuffer.Offset + actualCount, + _topBuffer.Count - actualCount); + totalRead += actualCount; + offset += actualCount; + count -= actualCount; + } + while (count > 0 && (_topBuffer.Count > 0 || _bufferedData.Count > 0)); + // Keep reading while there is more data available and we have more space to put it in. + return totalRead; + } + finally + { + registration.Dispose(); + _readLock.Release(); + } + } + + // Called under write-lock. + private void FirstWrite() + { + if (_firstWrite) + { + _firstWrite = false; + _onFirstWrite(); + } + } + + // Write with count 0 will still trigger OnFirstWrite + public override void Write(byte[] buffer, int offset, int count) + { + VerifyBuffer(buffer, offset, count, allowEmpty: true); + CheckDisposed(); + + _writeLock.Wait(); + try + { + FirstWrite(); + if (count == 0) + { + return; + } + // Copies are necessary because we don't know what the caller is going to do with the buffer afterwards. + byte[] internalBuffer = new byte[count]; + Buffer.BlockCopy(buffer, offset, internalBuffer, 0, count); + _bufferedData.Enqueue(internalBuffer); + + SignalDataAvailable(); + } + finally + { + _writeLock.Release(); + } + } +#if NET45 + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + Write(buffer, offset, count); + TaskCompletionSource tcs = new TaskCompletionSource(state); + tcs.TrySetResult(null); + IAsyncResult result = tcs.Task; + if (callback != null) + { + callback(result); + } + return result; + } + + public override void EndWrite(IAsyncResult asyncResult) + { + } +#endif + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + VerifyBuffer(buffer, offset, count, allowEmpty: true); + if (cancellationToken.IsCancellationRequested) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.TrySetCanceled(); + return tcs.Task; + } + + Write(buffer, offset, count); + return Task.FromResult(null); + } + + private static void VerifyBuffer(byte[] buffer, int offset, int count, bool allowEmpty) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentOutOfRangeException("offset", offset, string.Empty); + } + if (count < 0 || count > buffer.Length - offset + || (!allowEmpty && count == 0)) + { + throw new ArgumentOutOfRangeException("count", count, string.Empty); + } + } + + private void SignalDataAvailable() + { + // Dispatch, as TrySetResult will synchronously execute the waiters callback and block our Write. + Task.Factory.StartNew(() => _readWaitingForData.TrySetResult(null)); + } + + private Task WaitForDataAsync() + { + // Prevent race with Dispose + lock (_signalReadLock) + { + _readWaitingForData = new TaskCompletionSource(); + + if (!_bufferedData.IsEmpty || _disposed) + { + // Race, data could have arrived before we created the TCS. + _readWaitingForData.TrySetResult(null); + } + + return _readWaitingForData.Task; + } + } + + internal void Abort() + { + Abort(new OperationCanceledException()); + } + + internal void Abort(Exception innerException) + { + Contract.Requires(innerException != null); + _aborted = true; + _abortException = innerException; + Dispose(); + } + + private void CheckAborted() + { + if (_aborted) + { + throw new IOException(string.Empty, _abortException); + } + } + + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_writeLock", Justification = "ODEs from the locks would mask IOEs from abort.")] + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_readLock", Justification = "Data can still be read unless we get aborted.")] + protected override void Dispose(bool disposing) + { + if (disposing) + { + // Prevent race with WaitForDataAsync + lock (_signalReadLock) + { + // Throw for further writes, but not reads. Allow reads to drain the buffered data and then return 0 for further reads. + _disposed = true; + _readWaitingForData.TrySetResult(null); + } + } + + base.Dispose(disposing); + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + } + } +} diff --git a/src/Microsoft.AspNet.TestHost/TestClient.cs b/src/Microsoft.AspNet.TestHost/TestClient.cs index 7f9d7e8219..57886e56f9 100644 --- a/src/Microsoft.AspNet.TestHost/TestClient.cs +++ b/src/Microsoft.AspNet.TestHost/TestClient.cs @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; @@ -53,7 +39,7 @@ namespace Microsoft.AspNet.TestHost Action onSendingRequest = null) { var request = CreateRequest(method, uri, headers, body); - var response = new ResponseInformation(); + var response = new ResponseFeature(); var features = new FeatureCollection(); features.Add(typeof(IHttpRequestFeature), request); @@ -76,7 +62,7 @@ namespace Microsoft.AspNet.TestHost IDictionary headers, Stream body) { - var request = new RequestInformation(); + var request = new RequestFeature(); request.Method = method; request.Scheme = uri.Scheme; request.Path = PathString.FromUriComponent(uri).Value; diff --git a/src/Microsoft.AspNet.TestHost/TestServer.cs b/src/Microsoft.AspNet.TestHost/TestServer.cs index 48f1679f05..88f25d083f 100644 --- a/src/Microsoft.AspNet.TestHost/TestServer.cs +++ b/src/Microsoft.AspNet.TestHost/TestServer.cs @@ -1,31 +1,17 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Hosting.Server; -using Microsoft.AspNet.Hosting.Startup; using Microsoft.Framework.ConfigurationModel; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.Runtime; +using Microsoft.Framework.Runtime.Infrastructure; namespace Microsoft.AspNet.TestHost { @@ -35,6 +21,8 @@ namespace Microsoft.AspNet.TestHost private static readonly ServerInformation ServerInfo = new ServerInformation(); private Func _appDelegate; private TestClient _handler; + private IDisposable _appInstance; + private bool _disposed = false; public TestServer(IConfiguration config, IServiceProvider serviceProvider, Action appStartup) { @@ -54,16 +42,26 @@ namespace Microsoft.AspNet.TestHost }; var engine = serviceProvider.GetService(); - var disposable = engine.Start(hostContext); + _appInstance = engine.Start(hostContext); } - //public static TestServer Create(IServiceProvider provider) - //{ - // var startupLoader = new StartupLoader(provider, new NullStartupLoader()); - // var name = typeof(TStartup).AssemblyQualifiedName; - // var diagnosticMessages = new List(); - // return Create(provider, startupLoader.LoadStartup(name, "Test", diagnosticMessages)); - //} + public TestClient Handler + { + get + { + if (_handler == null) + { + _handler = new TestClient(Invoke); + } + + return _handler; + } + } + + public static TestServer Create(Action app) + { + return Create(provider: CallContextServiceLocator.Locator.ServiceProvider, app: app); + } public static TestServer Create(IServiceProvider provider, Action app) { @@ -77,17 +75,24 @@ namespace Microsoft.AspNet.TestHost return new TestServer(config, serviceProvider, app); } - public TestClient Handler + public HttpMessageHandler CreateHandler() { - get - { - if (_handler == null) - { - _handler = new TestClient(_appDelegate); - } + return new ClientHandler(Invoke); + } - return _handler; - } + public HttpClient CreateClient() + { + return new HttpClient(CreateHandler()) { BaseAddress = new Uri("http://localhost/") }; + } + + /// + /// Begins constructing a request message for submission. + /// + /// + /// to use in constructing additional request details. + public RequestBuilder CreateRequest(string path) + { + return new RequestBuilder(this, path); } public IServerInformation Initialize(IConfiguration configuration) @@ -107,11 +112,19 @@ namespace Microsoft.AspNet.TestHost return this; } + public Task Invoke(object env) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + return _appDelegate(env); + } + public void Dispose() { - // IServerFactory.Start needs to return an IDisposable. Typically this IDisposable instance is used to - // clear any server resources when tearing down the host. In our case we don't have anything to clear - // so we just implement IDisposable and do nothing. + _disposed = true; + _appInstance.Dispose(); } private class ServerInformation : IServerInformation diff --git a/src/Microsoft.AspNet.TestHost/project.json b/src/Microsoft.AspNet.TestHost/project.json index 1592b1a2c3..0effba0b63 100644 --- a/src/Microsoft.AspNet.TestHost/project.json +++ b/src/Microsoft.AspNet.TestHost/project.json @@ -1,7 +1,8 @@ { "version" : "1.0.0-*", "dependencies": { - "Microsoft.AspNet.Hosting": "" + "Microsoft.AspNet.Hosting": "", + "System.Net.Http": "4.0.0.0" }, "frameworks": { "net45": { }, diff --git a/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs b/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs new file mode 100644 index 0000000000..623a0bc395 --- /dev/null +++ b/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.FeatureModel; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.PipelineCore; +using Xunit; + +namespace Microsoft.AspNet.TestHost +{ + public class ClientHandlerTests + { + [Fact] + public Task ExpectedKeysAreAvailable() + { + var handler = new ClientHandler(env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + + // TODO: Assert.True(context.RequestAborted.CanBeCanceled); + Assert.Equal("HTTP/1.1", context.Request.Protocol); + Assert.Equal("GET", context.Request.Method); + Assert.Equal("https", context.Request.Scheme); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/A/Path/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(200, context.Response.StatusCode); + Assert.Null(context.GetFeature().ReasonPhrase); + Assert.Equal("example.com", context.Request.Host.Value); + + return Task.FromResult(0); + }); + var httpClient = new HttpClient(handler); + return httpClient.GetAsync("https://example.com/A/Path/and/file.txt?and=query"); + } + + [Fact] + public async Task ResubmitRequestWorks() + { + int requestCount = 1; + var handler = new ClientHandler(env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + int read = context.Request.Body.Read(new byte[100], 0, 100); + Assert.Equal(11, read); + + context.Response.Headers["TestHeader"] = "TestValue:" + requestCount++; + return Task.FromResult(0); + }); + + HttpMessageInvoker invoker = new HttpMessageInvoker(handler); + HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "https://example.com/"); + message.Content = new StringContent("Hello World"); + + HttpResponseMessage response = await invoker.SendAsync(message, CancellationToken.None); + Assert.Equal("TestValue:1", response.Headers.GetValues("TestHeader").First()); + + response = await invoker.SendAsync(message, CancellationToken.None); + Assert.Equal("TestValue:2", response.Headers.GetValues("TestHeader").First()); + } + + [Fact] + public async Task MiddlewareOnlySetsHeaders() + { + var handler = new ClientHandler(env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + + context.Response.Headers["TestHeader"] = "TestValue"; + return Task.FromResult(0); + }); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/"); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + } + + [Fact] + public async Task BlockingMiddlewareShouldNotBlockClient() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(env => + { + block.WaitOne(); + return Task.FromResult(0); + }); + var httpClient = new HttpClient(handler); + Task task = httpClient.GetAsync("https://example.com/"); + Assert.False(task.IsCompleted); + Assert.False(task.Wait(50)); + block.Set(); + HttpResponseMessage response = await task; + } + + [Fact] + public async Task HeadersAvailableBeforeBodyFinished() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(async env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + context.Response.Headers["TestHeader"] = "TestValue"; + await context.Response.WriteAsync("BodyStarted,"); + block.WaitOne(); + await context.Response.WriteAsync("BodyFinished"); + }); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + block.Set(); + Assert.Equal("BodyStarted,BodyFinished", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task FlushSendsHeaders() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(async env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + context.Response.Headers["TestHeader"] = "TestValue"; + context.Response.Body.Flush(); + block.WaitOne(); + await context.Response.WriteAsync("BodyFinished"); + }); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + block.Set(); + Assert.Equal("BodyFinished", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task ClientDisposalCloses() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + context.Response.Headers["TestHeader"] = "TestValue"; + context.Response.Body.Flush(); + block.WaitOne(); + return Task.FromResult(0); + }); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + Stream responseStream = await response.Content.ReadAsStreamAsync(); + Task readTask = responseStream.ReadAsync(new byte[100], 0, 100); + Assert.False(readTask.IsCompleted); + responseStream.Dispose(); + Thread.Sleep(50); + Assert.True(readTask.IsCompleted); + Assert.Equal(0, readTask.Result); + block.Set(); + } + + [Fact] + public async Task ClientCancellationAborts() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + context.Response.Headers["TestHeader"] = "TestValue"; + context.Response.Body.Flush(); + block.WaitOne(); + return Task.FromResult(0); + }); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + Stream responseStream = await response.Content.ReadAsStreamAsync(); + CancellationTokenSource cts = new CancellationTokenSource(); + Task readTask = responseStream.ReadAsync(new byte[100], 0, 100, cts.Token); + Assert.False(readTask.IsCompleted); + cts.Cancel(); + Thread.Sleep(50); + Assert.True(readTask.IsCompleted); + Assert.True(readTask.IsFaulted); + block.Set(); + } + + [Fact] + public Task ExceptionBeforeFirstWriteIsReported() + { + var handler = new ClientHandler(env => + { + throw new InvalidOperationException("Test Exception"); + }); + var httpClient = new HttpClient(handler); + return Assert.ThrowsAsync(() => httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead)); + } + + [Fact] + public async Task ExceptionAfterFirstWriteIsReported() + { + ManualResetEvent block = new ManualResetEvent(false); + var handler = new ClientHandler(async env => + { + var context = new DefaultHttpContext((IFeatureCollection)env); + context.Response.Headers["TestHeader"] = "TestValue"; + await context.Response.WriteAsync("BodyStarted"); + block.WaitOne(); + throw new InvalidOperationException("Test Exception"); + }); + var httpClient = new HttpClient(handler); + HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", + HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); + block.Set(); + var ex = await Assert.ThrowsAsync(() => response.Content.ReadAsStringAsync()); + Assert.IsType(ex.GetBaseException()); + } + } +} diff --git a/test/Microsoft.AspNet.TestHost.Tests/Microsoft.AspNet.TestHost.Tests.kproj b/test/Microsoft.AspNet.TestHost.Tests/Microsoft.AspNet.TestHost.Tests.kproj index 0ce16dcb3d..7ec4a03d46 100644 --- a/test/Microsoft.AspNet.TestHost.Tests/Microsoft.AspNet.TestHost.Tests.kproj +++ b/test/Microsoft.AspNet.TestHost.Tests/Microsoft.AspNet.TestHost.Tests.kproj @@ -21,11 +21,13 @@ + - + + - + \ No newline at end of file diff --git a/test/Microsoft.AspNet.TestHost.Tests/RequestBuilderTests.cs b/test/Microsoft.AspNet.TestHost.Tests/RequestBuilderTests.cs new file mode 100644 index 0000000000..0f637c7d24 --- /dev/null +++ b/test/Microsoft.AspNet.TestHost.Tests/RequestBuilderTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNet.TestHost +{ + public class RequestBuilderTests + { + [Fact] + public void AddRequestHeader() + { + TestServer server = TestServer.Create(app => { }); + server.CreateRequest("/") + .AddHeader("Host", "MyHost:90") + .And(request => + { + Assert.Equal("MyHost:90", request.Headers.Host.ToString()); + }); + } + + [Fact] + public void AddContentHeaders() + { + TestServer server = TestServer.Create(app => { }); + server.CreateRequest("/") + .AddHeader("Content-Type", "Test/Value") + .And(request => + { + Assert.NotNull(request.Content); + Assert.Equal("Test/Value", request.Content.Headers.ContentType.ToString()); + }); + } + } +} diff --git a/test/Microsoft.AspNet.TestHost.Tests/ResponseFeatureTests.cs b/test/Microsoft.AspNet.TestHost.Tests/ResponseFeatureTests.cs new file mode 100644 index 0000000000..ebf601cb82 --- /dev/null +++ b/test/Microsoft.AspNet.TestHost.Tests/ResponseFeatureTests.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.AspNet.TestHost +{ + public class ResponseFeatureTests + { + [Fact] + public void StatusCode_DefaultsTo200() + { + // Arrange & Act + var responseInformation = new ResponseFeature(); + + // Assert + Assert.Equal(200, responseInformation.StatusCode); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.TestHost.Tests/ResponseInformationTests.cs b/test/Microsoft.AspNet.TestHost.Tests/ResponseInformationTests.cs deleted file mode 100644 index 1717152773..0000000000 --- a/test/Microsoft.AspNet.TestHost.Tests/ResponseInformationTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using Xunit; - -namespace Microsoft.AspNet.TestHost.Tests -{ - public class ResponseInformationTests - { - [Fact] - public void StatusCode_DefaultsTo200() - { - // Arrange & Act - var responseInformation = new ResponseInformation(); - - // Assert - Assert.Equal(200, responseInformation.StatusCode); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNet.TestHost.Tests/TestApplicationEnvironment.cs b/test/Microsoft.AspNet.TestHost.Tests/TestApplicationEnvironment.cs index 800570f20a..417a903d0d 100644 --- a/test/Microsoft.AspNet.TestHost.Tests/TestApplicationEnvironment.cs +++ b/test/Microsoft.AspNet.TestHost.Tests/TestApplicationEnvironment.cs @@ -1,25 +1,11 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. +// Copyright (c) Microsoft Open Technologies, Inc. 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.Runtime.Versioning; using Microsoft.Framework.Runtime; -namespace Microsoft.AspNet.TestHost.Tests +namespace Microsoft.AspNet.TestHost { public class TestApplicationEnvironment : IApplicationEnvironment { diff --git a/test/Microsoft.AspNet.TestHost.Tests/TestClientTests.cs b/test/Microsoft.AspNet.TestHost.Tests/TestClientTests.cs index 48d416b266..96fbfc04b0 100644 --- a/test/Microsoft.AspNet.TestHost.Tests/TestClientTests.cs +++ b/test/Microsoft.AspNet.TestHost.Tests/TestClientTests.cs @@ -1,19 +1,5 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; @@ -26,7 +12,7 @@ using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.Runtime; using Xunit; -namespace Microsoft.AspNet.TestHost.Tests +namespace Microsoft.AspNet.TestHost { public class TestClientTests { diff --git a/test/Microsoft.AspNet.TestHost.Tests/TestServerTests.cs b/test/Microsoft.AspNet.TestHost.Tests/TestServerTests.cs index 13793b8cea..5f27cc89f1 100644 --- a/test/Microsoft.AspNet.TestHost.Tests/TestServerTests.cs +++ b/test/Microsoft.AspNet.TestHost.Tests/TestServerTests.cs @@ -1,22 +1,9 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. +// Copyright (c) Microsoft Open Technologies, Inc. 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.Net; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; @@ -25,7 +12,7 @@ using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.Runtime; using Xunit; -namespace Microsoft.AspNet.TestHost.Tests +namespace Microsoft.AspNet.TestHost { public class TestServerTests { @@ -41,24 +28,6 @@ namespace Microsoft.AspNet.TestHost.Tests Assert.DoesNotThrow(() => TestServer.Create(services, app => { })); } - //[Fact] - //public async Task CreateWithGeneric() - //{ - // // Arrange - // var services = new ServiceCollection() - // .AddSingleton() - // .BuildServiceProvider(); - - // var server = TestServer.Create(services); - // var client = server.Handler; - - // // Act - // var response = await client.GetAsync("http://any"); - - // // Assert - // Assert.Equal("Startup", new StreamReader(response.Body).ReadToEnd()); - //} - [Fact] public void ThrowsIfNoApplicationEnvironmentIsRegisteredWithTheProvider() { @@ -70,6 +39,72 @@ namespace Microsoft.AspNet.TestHost.Tests Assert.Throws(() => TestServer.Create(services, new Startup().Configuration)); } + [Fact] + public async Task CreateInvokesApp() + { + TestServer server = TestServer.Create(app => + { + app.Run(context => + { + return context.Response.WriteAsync("CreateInvokesApp"); + }); + }); + + string result = await server.CreateClient().GetStringAsync("/path"); + Assert.Equal("CreateInvokesApp", result); + } + + [Fact] + public async Task DisposeStreamIgnored() + { + TestServer server = TestServer.Create(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("Response"); + context.Response.Body.Dispose(); + }); + }); + + HttpResponseMessage result = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Equal("Response", await result.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task DisposedServerThrows() + { + TestServer server = TestServer.Create(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("Response"); + context.Response.Body.Dispose(); + }); + }); + + HttpResponseMessage result = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + server.Dispose(); + await Assert.ThrowsAsync(() => server.CreateClient().GetAsync("/")); + } + + [Fact] + public void CancelAborts() + { + TestServer server = TestServer.Create(app => + { + app.Run(context => + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + }); + }); + + Assert.Throws(() => { string result = server.CreateClient().GetStringAsync("/path").Result; }); + } + public class Startup { public void Configuration(IBuilder builder)