// 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.Linq; using System.Net.Http; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Xunit; namespace Microsoft.AspNetCore.TestHost { public class TestClientTests { [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task GetAsyncWorks() { // Arrange var expected = "GET Response"; RequestDelegate appDelegate = ctx => ctx.Response.WriteAsync(expected); var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); // Act var actual = await client.GetStringAsync("http://localhost:12345"); // Assert Assert.Equal(expected, actual); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task NoTrailingSlash_NoPathBase() { // Arrange var expected = "GET Response"; RequestDelegate appDelegate = ctx => { Assert.Equal("", ctx.Request.PathBase.Value); Assert.Equal("/", ctx.Request.Path.Value); return ctx.Response.WriteAsync(expected); }; var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); // Act var actual = await client.GetStringAsync("http://localhost:12345"); // Assert Assert.Equal(expected, actual); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task SingleTrailingSlash_NoPathBase() { // Arrange var expected = "GET Response"; RequestDelegate appDelegate = ctx => { Assert.Equal("", ctx.Request.PathBase.Value); Assert.Equal("/", ctx.Request.Path.Value); return ctx.Response.WriteAsync(expected); }; var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); // Act var actual = await client.GetStringAsync("http://localhost:12345/"); // Assert Assert.Equal(expected, actual); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task PutAsyncWorks() { // Arrange RequestDelegate appDelegate = ctx => ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " PUT Response"); var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); // Act var content = new StringContent("Hello world"); var response = await client.PutAsync("http://localhost:12345", content); // Assert Assert.Equal("Hello world PUT Response", await response.Content.ReadAsStringAsync()); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task PostAsyncWorks() { // Arrange RequestDelegate appDelegate = async ctx => await ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " POST Response"); var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); // Act var content = new StringContent("Hello world"); var response = await client.PostAsync("http://localhost:12345", content); // Assert Assert.Equal("Hello world POST Response", await response.Content.ReadAsStringAsync()); } [Fact] public async Task LargePayload_DisposesRequest_AfterResponseIsCompleted() { // Arrange var data = new byte[2048]; var character = Encoding.ASCII.GetBytes("a"); for (var i = 0; i < data.Length; i++) { data[i] = character[0]; } var builder = new WebHostBuilder(); RequestDelegate app = (ctx) => { var disposable = new TestDisposable(); ctx.Response.RegisterForDispose(disposable); ctx.Response.Body.Write(data, 0, 1024); Assert.False(disposable.IsDisposed); ctx.Response.Body.Write(data, 1024, 1024); return Task.FromResult(0); }; builder.Configure(appBuilder => appBuilder.Run(app)); var server = new TestServer(builder); var client = server.CreateClient(); // Act & Assert var response = await client.GetAsync("http://localhost:12345"); } private class TestDisposable : IDisposable { public bool IsDisposed { get; private set; } public void Dispose() { IsDisposed = true; } } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task WebSocketWorks() { // Arrange // This logger will attempt to access information from HttpRequest once the HttpContext is created var logger = new VerifierLogger(); RequestDelegate appDelegate = async ctx => { if (ctx.WebSockets.IsWebSocketRequest) { var websocket = await ctx.WebSockets.AcceptWebSocketAsync(); var receiveArray = new byte[1024]; while (true) { var receiveResult = await websocket.ReceiveAsync(new System.ArraySegment(receiveArray), CancellationToken.None); if (receiveResult.MessageType == WebSocketMessageType.Close) { await websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Normal Closure", CancellationToken.None); break; } else { var sendBuffer = new System.ArraySegment(receiveArray, 0, receiveResult.Count); await websocket.SendAsync(sendBuffer, receiveResult.MessageType, receiveResult.EndOfMessage, CancellationToken.None); } } } }; var builder = new WebHostBuilder() .ConfigureServices(services => { services.AddSingleton>(logger); }) .Configure(app => { app.Run(appDelegate); }); var server = new TestServer(builder); // Act var client = server.CreateWebSocketClient(); // The HttpContext will be created and the logger will make sure that the HttpRequest exists and contains reasonable values var clientSocket = await client.ConnectAsync(new System.Uri("http://localhost"), CancellationToken.None); var hello = Encoding.UTF8.GetBytes("hello"); await clientSocket.SendAsync(new System.ArraySegment(hello), WebSocketMessageType.Text, true, CancellationToken.None); var world = Encoding.UTF8.GetBytes("world!"); await clientSocket.SendAsync(new System.ArraySegment(world), WebSocketMessageType.Binary, true, CancellationToken.None); await clientSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Normal Closure", CancellationToken.None); // Assert Assert.Equal(WebSocketState.CloseSent, clientSocket.State); var buffer = new byte[1024]; var result = await clientSocket.ReceiveAsync(new System.ArraySegment(buffer), CancellationToken.None); Assert.Equal(hello.Length, result.Count); Assert.True(hello.SequenceEqual(buffer.Take(hello.Length))); Assert.Equal(WebSocketMessageType.Text, result.MessageType); result = await clientSocket.ReceiveAsync(new System.ArraySegment(buffer), CancellationToken.None); Assert.Equal(world.Length, result.Count); Assert.True(world.SequenceEqual(buffer.Take(world.Length))); Assert.Equal(WebSocketMessageType.Binary, result.MessageType); result = await clientSocket.ReceiveAsync(new System.ArraySegment(buffer), CancellationToken.None); Assert.Equal(WebSocketMessageType.Close, result.MessageType); Assert.Equal(WebSocketState.Closed, clientSocket.State); clientSocket.Dispose(); } [ConditionalFact] public async Task WebSocketAcceptThrowsWhenCancelled() { // Arrange // This logger will attempt to access information from HttpRequest once the HttpContext is created var logger = new VerifierLogger(); RequestDelegate appDelegate = async ctx => { if (ctx.WebSockets.IsWebSocketRequest) { var websocket = await ctx.WebSockets.AcceptWebSocketAsync(); var receiveArray = new byte[1024]; while (true) { var receiveResult = await websocket.ReceiveAsync(new System.ArraySegment(receiveArray), CancellationToken.None); if (receiveResult.MessageType == WebSocketMessageType.Close) { await websocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Normal Closure", CancellationToken.None); break; } else { var sendBuffer = new System.ArraySegment(receiveArray, 0, receiveResult.Count); await websocket.SendAsync(sendBuffer, receiveResult.MessageType, receiveResult.EndOfMessage, CancellationToken.None); } } } }; var builder = new WebHostBuilder() .ConfigureServices(services => { services.AddSingleton>(logger); }) .Configure(app => { app.Run(appDelegate); }); var server = new TestServer(builder); // Act var client = server.CreateWebSocketClient(); var tokenSource = new CancellationTokenSource(); tokenSource.Cancel(); // Assert await Assert.ThrowsAnyAsync(async () => await client.ConnectAsync(new System.Uri("http://localhost"), tokenSource.Token)); } private class VerifierLogger : ILogger { public IDisposable BeginScope(TState state) => new NoopDispoasble(); public bool IsEnabled(LogLevel logLevel) => true; // This call verifies that fields of HttpRequest are accessed and valid public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) => formatter(state, exception); class NoopDispoasble : IDisposable { public void Dispose() { } } } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task WebSocketDisposalThrowsOnPeer() { // Arrange RequestDelegate appDelegate = async ctx => { if (ctx.WebSockets.IsWebSocketRequest) { var websocket = await ctx.WebSockets.AcceptWebSocketAsync(); websocket.Dispose(); } }; var builder = new WebHostBuilder().Configure(app => { app.Run(appDelegate); }); var server = new TestServer(builder); // Act var client = server.CreateWebSocketClient(); var clientSocket = await client.ConnectAsync(new System.Uri("http://localhost"), CancellationToken.None); var buffer = new byte[1024]; await Assert.ThrowsAsync(async () => await clientSocket.ReceiveAsync(new System.ArraySegment(buffer), CancellationToken.None)); clientSocket.Dispose(); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task WebSocketTinyReceiveGeneratesEndOfMessage() { // Arrange RequestDelegate appDelegate = async ctx => { if (ctx.WebSockets.IsWebSocketRequest) { var websocket = await ctx.WebSockets.AcceptWebSocketAsync(); var receiveArray = new byte[1024]; while (true) { var receiveResult = await websocket.ReceiveAsync(new System.ArraySegment(receiveArray), CancellationToken.None); var sendBuffer = new System.ArraySegment(receiveArray, 0, receiveResult.Count); await websocket.SendAsync(sendBuffer, receiveResult.MessageType, receiveResult.EndOfMessage, CancellationToken.None); } } }; var builder = new WebHostBuilder().Configure(app => { app.Run(appDelegate); }); var server = new TestServer(builder); // Act var client = server.CreateWebSocketClient(); var clientSocket = await client.ConnectAsync(new System.Uri("http://localhost"), CancellationToken.None); var hello = Encoding.UTF8.GetBytes("hello"); await clientSocket.SendAsync(new System.ArraySegment(hello), WebSocketMessageType.Text, true, CancellationToken.None); // Assert var buffer = new byte[1]; for (var i = 0; i < hello.Length; i++) { bool last = i == (hello.Length - 1); var result = await clientSocket.ReceiveAsync(new System.ArraySegment(buffer), CancellationToken.None); Assert.Equal(buffer.Length, result.Count); Assert.Equal(buffer[0], hello[i]); Assert.Equal(last, result.EndOfMessage); } clientSocket.Dispose(); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task ClientDisposalAbortsRequest() { // Arrange TaskCompletionSource tcs = new TaskCompletionSource(); RequestDelegate appDelegate = async ctx => { // Write Headers await ctx.Response.Body.FlushAsync(); var sem = new SemaphoreSlim(0); try { await sem.WaitAsync(ctx.RequestAborted); } catch (Exception e) { tcs.SetException(e); } }; // Act var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:12345"); var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); // Abort Request response.Dispose(); // Assert var exception = await Assert.ThrowsAnyAsync(async () => await tcs.Task); } [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono, SkipReason = "Hangs randomly (issue #507)")] public async Task ClientCancellationAbortsRequest() { // Arrange TaskCompletionSource tcs = new TaskCompletionSource(); RequestDelegate appDelegate = async ctx => { var sem = new SemaphoreSlim(0); try { await sem.WaitAsync(ctx.RequestAborted); } catch (Exception e) { tcs.SetException(e); } }; // Act var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); var cts = new CancellationTokenSource(); cts.CancelAfter(500); var response = await client.GetAsync("http://localhost:12345", cts.Token); // Assert var exception = await Assert.ThrowsAnyAsync(async () => await tcs.Task); } } }