diff --git a/src/Mvc/Mvc.Core/src/FileContentResult.cs b/src/Mvc/Mvc.Core/src/FileContentResult.cs index f9bc761110b6..57ea2f943d9b 100644 --- a/src/Mvc/Mvc.Core/src/FileContentResult.cs +++ b/src/Mvc/Mvc.Core/src/FileContentResult.cs @@ -89,7 +89,7 @@ Task IResult.ExecuteAsync(HttpContext httpContext) } var loggerFactory = httpContext.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog( httpContext, diff --git a/src/Mvc/Mvc.Core/src/FileStreamResult.cs b/src/Mvc/Mvc.Core/src/FileStreamResult.cs index ba358eeb09c4..00c5732e7626 100644 --- a/src/Mvc/Mvc.Core/src/FileStreamResult.cs +++ b/src/Mvc/Mvc.Core/src/FileStreamResult.cs @@ -5,8 +5,10 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc @@ -15,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc /// Represents an that when executed will /// write a file from a stream to the response. /// - public class FileStreamResult : FileResult + public class FileStreamResult : FileResult, IResult { private Stream _fileStream; @@ -79,5 +81,37 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = context.HttpContext.RequestServices.GetRequiredService>(); return executor.ExecuteAsync(context, this); } + + async Task IResult.ExecuteAsync(HttpContext httpContext) + { + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + + Task writeFileAsync(HttpContext httpContext, FileStreamResult result, RangeItemHeaderValue? range, long rangeLength) + => FileStreamResultExecutor.WriteFileAsyncInternal(httpContext, this, range, rangeLength, logger!); + + (RangeItemHeaderValue? range, long rangeLength, bool serveBody) setHeadersAndLog( + HttpContext httpContext, + FileResult result, + long? fileLength, + bool enableRangeProcessing, + DateTimeOffset? lastModified, + EntityTagHeaderValue? etag) + => FileResultExecutorBase.SetHeadersAndLog( + httpContext, + this, + fileLength, + EnableRangeProcessing, + LastModified, + EntityTag, + logger!); + + await FileStreamResultExecutor.ExecuteAsyncInternal( + httpContext, + this, + setHeadersAndLog, + writeFileAsync, + logger); + } } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/FileStreamResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/FileStreamResultExecutor.cs index ce6295cf0f51..bad35988e8c7 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/FileStreamResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/FileStreamResultExecutor.cs @@ -5,6 +5,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -26,6 +27,37 @@ public FileStreamResultExecutor(ILoggerFactory loggerFactory) /// public virtual async Task ExecuteAsync(ActionContext context, FileStreamResult result) + { + await ExecuteAsyncInternal( + context, + result, + SetHeadersAndLog, + WriteFileAsync, + Logger); + } + + /// + /// Write the contents of the FileStreamResult to the response body. + /// + /// The . + /// The FileStreamResult to write. + /// The . + /// The range length. + protected virtual Task WriteFileAsync( + ActionContext context, + FileStreamResult result, + RangeItemHeaderValue? range, + long rangeLength) + { + return WriteFileAsyncInternal(context.HttpContext, result, range, rangeLength, Logger); + } + + internal static async Task ExecuteAsyncInternal( + TContext context, + FileStreamResult result, + Func SetHeadersAndLog, + Func WriteFileAsync, + ILogger logger) { if (context == null) { @@ -39,7 +71,7 @@ public virtual async Task ExecuteAsync(ActionContext context, FileStreamResult r using (result.FileStream) { - Logger.ExecutingFileResult(result); + logger.ExecutingFileResult(result); long? fileLength = null; if (result.FileStream.CanSeek) @@ -64,22 +96,11 @@ public virtual async Task ExecuteAsync(ActionContext context, FileStreamResult r } } - /// - /// Write the contents of the FileStreamResult to the response body. - /// - /// The . - /// The FileStreamResult to write. - /// The . - /// The range length. - protected virtual Task WriteFileAsync( - ActionContext context, - FileStreamResult result, - RangeItemHeaderValue? range, - long rangeLength) + internal static Task WriteFileAsyncInternal(HttpContext httpContext, FileStreamResult result, RangeItemHeaderValue? range, long rangeLength, ILogger logger) { - if (context == null) + if (httpContext == null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(httpContext)); } if (result == null) @@ -94,10 +115,10 @@ protected virtual Task WriteFileAsync( if (range != null) { - Logger.WritingRangeToBody(); + logger.WritingRangeToBody(); } - return WriteFileAsync(context.HttpContext, result.FileStream, range, rangeLength); + return WriteFileAsync(httpContext, result.FileStream, range, rangeLength); } } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/LocalRedirectResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/LocalRedirectResultExecutor.cs index 2584c3f8e24f..14a0f19b08e0 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/LocalRedirectResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/LocalRedirectResultExecutor.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Infrastructure { @@ -45,9 +44,27 @@ public LocalRedirectResultExecutor(ILoggerFactory loggerFactory, IUrlHelperFacto /// public virtual Task ExecuteAsync(ActionContext context, LocalRedirectResult result) { - if (context == null) + var urlHelper = result.UrlHelper ?? _urlHelperFactory.GetUrlHelper(context); + + return ExecuteAsyncInternal( + context.HttpContext, + result, + urlHelper.IsLocalUrl, + urlHelper.Content, + _logger); + } + + internal static Task ExecuteAsyncInternal( + HttpContext httpContext, + LocalRedirectResult result, + Func isLocalUrl, + Func getContent, + ILogger logger + ) + { + if (httpContext == null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(httpContext)); } if (result == null) @@ -55,26 +72,24 @@ public virtual Task ExecuteAsync(ActionContext context, LocalRedirectResult resu throw new ArgumentNullException(nameof(result)); } - var urlHelper = result.UrlHelper ?? _urlHelperFactory.GetUrlHelper(context); - // IsLocalUrl is called to handle Urls starting with '~/'. - if (!urlHelper.IsLocalUrl(result.Url)) + if (!isLocalUrl(result.Url)) { throw new InvalidOperationException(Resources.UrlNotLocal); } - var destinationUrl = urlHelper.Content(result.Url); - _logger.LocalRedirectResultExecuting(destinationUrl); + var destinationUrl = getContent(result.Url); + logger.LocalRedirectResultExecuting(destinationUrl); if (result.PreserveMethod) { - context.HttpContext.Response.StatusCode = result.Permanent ? + httpContext.Response.StatusCode = result.Permanent ? StatusCodes.Status308PermanentRedirect : StatusCodes.Status307TemporaryRedirect; - context.HttpContext.Response.Headers.Location = destinationUrl; + httpContext.Response.Headers.Location = destinationUrl; } else { - context.HttpContext.Response.Redirect(destinationUrl, result.Permanent); + httpContext.Response.Redirect(destinationUrl, result.Permanent); } return Task.CompletedTask; diff --git a/src/Mvc/Mvc.Core/src/LocalRedirectResult.cs b/src/Mvc/Mvc.Core/src/LocalRedirectResult.cs index 5efbb523d6dc..72ff1620858a 100644 --- a/src/Mvc/Mvc.Core/src/LocalRedirectResult.cs +++ b/src/Mvc/Mvc.Core/src/LocalRedirectResult.cs @@ -4,9 +4,12 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc { @@ -14,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc /// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), /// or Permanent Redirect (308) response with a Location header to the supplied local URL. /// - public class LocalRedirectResult : ActionResult + public class LocalRedirectResult : ActionResult, IResult { private string _localUrl; @@ -103,5 +106,18 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = context.HttpContext.RequestServices.GetRequiredService>(); return executor.ExecuteAsync(context, this); } + + Task IResult.ExecuteAsync(HttpContext httpContext) + { + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + + return LocalRedirectResultExecutor.ExecuteAsyncInternal( + httpContext, + this, + UrlHelperBase.CheckIsLocalUrl, + url => UrlHelperBase.Content(httpContext, Url), + logger); + } } } diff --git a/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs b/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs index 9734ddbacdfe..ca1149a002aa 100644 --- a/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs +++ b/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs @@ -95,7 +95,7 @@ internal static Task ExecuteAsyncInternal( long fileLength) { var loggerFactory = httpContext.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); logger.ExecutingFileResult(result, result.FileName); diff --git a/src/Mvc/Mvc.Core/src/VirtualFileResult.cs b/src/Mvc/Mvc.Core/src/VirtualFileResult.cs index ac95c84a07eb..8d6f17f28441 100644 --- a/src/Mvc/Mvc.Core/src/VirtualFileResult.cs +++ b/src/Mvc/Mvc.Core/src/VirtualFileResult.cs @@ -92,7 +92,7 @@ Task IResult.ExecuteAsync(HttpContext httpContext) } var loggerFactory = httpContext.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(); + var logger = loggerFactory.CreateLogger(); var lastModified = LastModified ?? fileInfo.LastModified; var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog( diff --git a/src/Mvc/Mvc.Core/test/BaseFileStreamResultTest.cs b/src/Mvc/Mvc.Core/test/BaseFileStreamResultTest.cs new file mode 100644 index 000000000000..cd630c7dfc58 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/BaseFileStreamResultTest.cs @@ -0,0 +1,584 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class BaseFileStreamResultTest + { + public static async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested( + long? start, + long? end, + string expectedString, + long contentLength, + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + readStream.SetLength(11); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = true, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.Range = new RangeHeaderValue(start, end); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + start = start ?? 11 - end; + end = start + contentLength - 1; + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); + Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); + Assert.Equal(contentLength, httpResponse.ContentLength); + Assert.Equal(expectedString, body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange( + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = DateTimeOffset.MinValue; + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + readStream.SetLength(11); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = true, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + requestHeaders.Range = new RangeHeaderValue(0, 4); + requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length); + Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); + Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); + Assert.Equal(5, httpResponse.ContentLength); + Assert.Equal("Hello", body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored( + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = DateTimeOffset.MinValue; + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + readStream.SetLength(11); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + requestHeaders.Range = new RangeHeaderValue(0, 4); + requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue); + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + Assert.Equal("Hello World", body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored( + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = DateTimeOffset.MinValue.AddDays(1); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + readStream.SetLength(11); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = true, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + requestHeaders.Range = new RangeHeaderValue(0, 4); + requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue); + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + Assert.Equal("Hello World", body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored( + string rangeString, + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = true, + }; + + var httpContext = GetHttpContext(); + httpContext.Request.Headers.Range = rangeString; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Empty(httpResponse.Headers.ContentRange); + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + Assert.Equal("Hello World", body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable( + string rangeString, + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = true, + }; + + var httpContext = GetHttpContext(); + httpContext.Request.Headers.Range = rangeString; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + var contentRange = new ContentRangeHeaderValue(byteArray.Length); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); + Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); + Assert.Equal(0, httpResponse.ContentLength); + Assert.Empty(body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_RangeRequested_PreconditionFailed( + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = true, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"NotEtag\""), + }; + httpContext.Request.Headers.Range = "bytes = 0-6"; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode); + Assert.Null(httpResponse.ContentLength); + Assert.Empty(httpResponse.Headers.ContentRange); + Assert.NotEmpty(httpResponse.Headers.LastModified); + Assert.Empty(body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_NotModified_RangeRequestedIgnored( + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = true, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfNoneMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + httpContext.Request.Headers.Range = "bytes = 0-6"; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode); + Assert.Null(httpResponse.ContentLength); + Assert.Empty(httpResponse.Headers.ContentRange); + Assert.False(httpResponse.Headers.ContainsKey(HeaderNames.ContentType)); + Assert.NotEmpty(httpResponse.Headers.LastModified); + Assert.Empty(body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull( + long? fileLength, + Func function) + { + // Arrange + var contentType = "text/plain"; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes(""); + var readStream = new MemoryStream(byteArray); + fileLength = fileLength ?? 0L; + readStream.SetLength(fileLength.Value); + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = true, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.Range = new RangeHeaderValue(0, 5); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + var contentRange = new ContentRangeHeaderValue(byteArray.Length); + Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); + Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); + Assert.Equal(0, httpResponse.ContentLength); + Assert.Empty(body); + Assert.False(readStream.CanSeek); + } + + public static async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes( + Func function) + { + // Arrange + var mockReadStream = new Mock(); + mockReadStream.SetupSequence(s => s.ReadAsync(It.IsAny(), 0, 0x1000, CancellationToken.None)) + .Returns(Task.FromResult(0x1000)) + .Returns(Task.FromResult(0x500)) + .Returns(Task.FromResult(0)); + + var mockBodyStream = new Mock(); + mockBodyStream + .Setup(s => s.WriteAsync(It.IsAny(), 0, 0x1000, CancellationToken.None)) + .Returns(Task.FromResult(0)); + + mockBodyStream + .Setup(s => s.WriteAsync(It.IsAny(), 0, 0x500, CancellationToken.None)) + .Returns(Task.FromResult(0)); + + var result = new FileStreamResult(mockReadStream.Object, "text/plain"); + + var httpContext = GetHttpContext(); + httpContext.Response.Body = mockBodyStream.Object; + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + mockReadStream.Verify(); + mockBodyStream.Verify(); + } + + public static async Task WriteFileAsync_CopiesProvidedStream_ToOutputStream( + Func function) + { + // Arrange + // Generate an array of bytes with a predictable pattern + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13 + var originalBytes = Enumerable.Range(0, 0x1234) + .Select(b => (byte)(b % 20)).ToArray(); + + var originalStream = new MemoryStream(originalBytes); + + var httpContext = GetHttpContext(); + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + var result = new FileStreamResult(originalStream, "text/plain"); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var outBytes = outStream.ToArray(); + Assert.True(originalBytes.SequenceEqual(outBytes)); + Assert.False(originalStream.CanSeek); + } + + public static async Task SetsSuppliedContentTypeAndEncoding( + Func function) + { + // Arrange + var expectedContentType = "text/foo; charset=us-ascii"; + // Generate an array of bytes with a predictable pattern + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13 + var originalBytes = Enumerable.Range(0, 0x1234) + .Select(b => (byte)(b % 20)).ToArray(); + + var originalStream = new MemoryStream(originalBytes); + + var httpContext = GetHttpContext(); + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + var result = new FileStreamResult(originalStream, expectedContentType); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + var outBytes = outStream.ToArray(); + Assert.True(originalBytes.SequenceEqual(outBytes)); + Assert.Equal(expectedContentType, httpContext.Response.ContentType); + Assert.False(originalStream.CanSeek); + } + + public static async Task HeadRequest_DoesNotWriteToBody_AndClosesReadStream( + Func function) + { + // Arrange + var readStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello, World!")); + + var httpContext = GetHttpContext(); + httpContext.Request.Method = "HEAD"; + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + var result = new FileStreamResult(readStream, "text/plain"); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + Assert.False(readStream.CanSeek); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(0, httpContext.Response.Body.Length); + } + + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton, FileStreamResultExecutor>(); + services.AddSingleton(NullLoggerFactory.Instance); + return services; + } + + private static HttpContext GetHttpContext() + { + var services = CreateServices(); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services.BuildServiceProvider(); + return httpContext; + } + } +} diff --git a/src/Mvc/Mvc.Core/test/BaseLocalRedirectResultTest.cs b/src/Mvc/Mvc.Core/test/BaseLocalRedirectResultTest.cs new file mode 100644 index 000000000000..0a5f2d9dc5b4 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/BaseLocalRedirectResultTest.cs @@ -0,0 +1,106 @@ +// 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.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class BaseLocalRedirectResultTest + { + public static async Task Execute_ReturnsExpectedValues( + Func function) + { + // Arrange + var appRoot = "/"; + var contentPath = "~/Home/About"; + var expectedPath = "/Home/About"; + + var httpContext = GetHttpContext(appRoot); + var actionContext = GetActionContext(httpContext); + var result = new LocalRedirectResult(contentPath); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString()); + Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + } + + public static async Task Execute_Throws_ForNonLocalUrl( + string appRoot, + string contentPath, + Func function) + { + // Arrange + var httpContext = GetHttpContext(appRoot); + var actionContext = GetActionContext(httpContext); + var result = new LocalRedirectResult(contentPath); + + // Act & Assert + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + var exception = await Assert.ThrowsAsync(() => function(result, (TContext)context)); + Assert.Equal( + "The supplied URL is not local. A URL with an absolute path is considered local if it does not " + + "have a host/authority part. URLs using virtual paths ('~/') are also local.", + exception.Message); + } + + public static async Task Execute_Throws_ForNonLocalUrlTilde( + string appRoot, + string contentPath, + Func function) + { + // Arrange + var httpContext = GetHttpContext(appRoot); + var actionContext = GetActionContext(httpContext); + var result = new LocalRedirectResult(contentPath); + + // Act & Assert + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + + var exception = await Assert.ThrowsAsync(() => function(result, (TContext)context)); + Assert.Equal( + "The supplied URL is not local. A URL with an absolute path is considered local if it does not " + + "have a host/authority part. URLs using virtual paths ('~/') are also local.", + exception.Message); + } + + private static ActionContext GetActionContext(HttpContext httpContext) + { + var routeData = new RouteData(); + routeData.Routers.Add(new Mock().Object); + + return new ActionContext(httpContext, routeData, new ActionDescriptor()); + } + + private static IServiceProvider GetServiceProvider() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton, LocalRedirectResultExecutor>(); + serviceCollection.AddSingleton(); + serviceCollection.AddTransient(); + return serviceCollection.BuildServiceProvider(); + } + + private static HttpContext GetHttpContext( + string appRoot) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = GetServiceProvider(); + httpContext.Request.PathBase = new PathString(appRoot); + return httpContext; + } + } +} diff --git a/src/Mvc/Mvc.Core/test/FileStreamActionResultTest.cs b/src/Mvc/Mvc.Core/test/FileStreamActionResultTest.cs new file mode 100644 index 000000000000..f78c07b7cb72 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/FileStreamActionResultTest.cs @@ -0,0 +1,186 @@ +// 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.Tasks; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class FileStreamActionResultTest + { + [Fact] + public void Constructor_SetsFileName() + { + // Arrange + var stream = Stream.Null; + + // Act + var result = new FileStreamResult(stream, "text/plain"); + + // Assert + Assert.Equal(stream, result.FileStream); + } + + [Fact] + public void Constructor_SetsContentTypeAndParameters() + { + // Arrange + var stream = Stream.Null; + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var expectedMediaType = contentType; + + // Act + var result = new FileStreamResult(stream, contentType); + + // Assert + Assert.Equal(stream, result.FileStream); + MediaTypeAssert.Equal(expectedMediaType, result.ContentType); + } + + [Fact] + public void Constructor_SetsLastModifiedAndEtag() + { + // Arrange + var stream = Stream.Null; + var contentType = "text/plain"; + var expectedMediaType = contentType; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + + // Act + var result = new FileStreamResult(stream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + }; + + // Assert + Assert.Equal(lastModified, result.LastModified); + Assert.Equal(entityTag, result.EntityTag); + MediaTypeAssert.Equal(expectedMediaType, result.ContentType); + } + + [Theory] + [InlineData(0, 4, "Hello", 5)] + [InlineData(6, 10, "World", 5)] + [InlineData(null, 5, "World", 5)] + [InlineData(6, null, "World", 5)] + public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested( + start, + end, + expectedString, + contentLength, + action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action); + } + + [Fact] + public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action); + } + + [Theory] + [InlineData("0-5")] + [InlineData("bytes = ")] + [InlineData("bytes = 1-4, 5-11")] + public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(rangeString, action); + } + + [Theory] + [InlineData("bytes = 12-13")] + [InlineData("bytes = -0")] + public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(rangeString, action); + } + + [Fact] + public async Task WriteFileAsync_RangeRequested_PreconditionFailed() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action); + } + + [Fact] + public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_NotModified_RangeRequestedIgnored(action); + } + + [Theory] + [InlineData(0)] + [InlineData(null)] + public async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull(long? fileLength) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_RangeRequested_FileLengthZeroOrNull(fileLength, action); + } + + [Fact] + public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_WritesResponse_InChunksOfFourKilobytes(action); + } + + [Fact] + public async Task WriteFileAsync_CopiesProvidedStream_ToOutputStream() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.WriteFileAsync_CopiesProvidedStream_ToOutputStream(action); + } + + [Fact] + public async Task SetsSuppliedContentTypeAndEncoding() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.SetsSuppliedContentTypeAndEncoding(action); + } + + [Fact] + public async Task HeadRequest_DoesNotWriteToBody_AndClosesReadStream() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileStreamResultTest.HeadRequest_DoesNotWriteToBody_AndClosesReadStream(action); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/FileStreamResultTest.cs b/src/Mvc/Mvc.Core/test/FileStreamResultTest.cs index fc40761013a3..23c2569530f4 100644 --- a/src/Mvc/Mvc.Core/test/FileStreamResultTest.cs +++ b/src/Mvc/Mvc.Core/test/FileStreamResultTest.cs @@ -3,77 +3,15 @@ using System; using System.IO; -using System.Linq; -using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Net.Http.Headers; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc { public class FileStreamResultTest { - [Fact] - public void Constructor_SetsFileName() - { - // Arrange - var stream = Stream.Null; - - // Act - var result = new FileStreamResult(stream, "text/plain"); - - // Assert - Assert.Equal(stream, result.FileStream); - } - - [Fact] - public void Constructor_SetsContentTypeAndParameters() - { - // Arrange - var stream = Stream.Null; - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var expectedMediaType = contentType; - - // Act - var result = new FileStreamResult(stream, contentType); - - // Assert - Assert.Equal(stream, result.FileStream); - MediaTypeAssert.Equal(expectedMediaType, result.ContentType); - } - - [Fact] - public void Constructor_SetsLastModifiedAndEtag() - { - // Arrange - var stream = Stream.Null; - var contentType = "text/plain"; - var expectedMediaType = contentType; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - - // Act - var result = new FileStreamResult(stream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - }; - - // Assert - Assert.Equal(lastModified, result.LastModified); - Assert.Equal(entityTag, result.EntityTag); - MediaTypeAssert.Equal(expectedMediaType, result.ContentType); - } - [Theory] [InlineData(0, 4, "Hello", 5)] [InlineData(6, 10, "World", 5)] @@ -81,189 +19,38 @@ public void Constructor_SetsLastModifiedAndEtag() [InlineData(6, null, "World", 5)] public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength) { - // Arrange - var contentType = "text/plain"; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes("Hello World"); - var readStream = new MemoryStream(byteArray); - readStream.SetLength(11); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = true, - }; - - var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.Range = new RangeHeaderValue(start, end); - requestHeaders.IfMatch = new[] - { - new EntityTagHeaderValue("\"Etag\""), - }; - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - start = start ?? 11 - end; - end = start + contentLength - 1; - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = streamReader.ReadToEndAsync().Result; - var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, byteArray.Length); - Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); - Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); - Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); - Assert.Equal(contentLength, httpResponse.ContentLength); - Assert.Equal(expectedString, body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested( + start, + end, + expectedString, + contentLength, + action); } [Fact] public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() { - // Arrange - var contentType = "text/plain"; - var lastModified = DateTimeOffset.MinValue; - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes("Hello World"); - var readStream = new MemoryStream(byteArray); - readStream.SetLength(11); - - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = true, - }; - - var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfMatch = new[] - { - new EntityTagHeaderValue("\"Etag\""), - }; - requestHeaders.Range = new RangeHeaderValue(0, 4); - requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = streamReader.ReadToEndAsync().Result; - Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length); - Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); - Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); - Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); - Assert.Equal(5, httpResponse.ContentLength); - Assert.Equal("Hello", body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action); } [Fact] public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored() { - // Arrange - var contentType = "text/plain"; - var lastModified = DateTimeOffset.MinValue; - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes("Hello World"); - var readStream = new MemoryStream(byteArray); - readStream.SetLength(11); - - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - }; - - var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfMatch = new[] - { - new EntityTagHeaderValue("\"Etag\""), - }; - requestHeaders.Range = new RangeHeaderValue(0, 4); - requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue); - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Assert - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = streamReader.ReadToEndAsync().Result; - Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); - Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - Assert.Equal("Hello World", body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action); } [Fact] public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() { - // Arrange - var contentType = "text/plain"; - var lastModified = DateTimeOffset.MinValue.AddDays(1); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes("Hello World"); - var readStream = new MemoryStream(byteArray); - readStream.SetLength(11); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = true, - }; - - var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfMatch = new[] - { - new EntityTagHeaderValue("\"Etag\""), - }; - requestHeaders.Range = new RangeHeaderValue(0, 4); - requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue); - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = streamReader.ReadToEndAsync().Result; - Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); - Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - Assert.Equal("Hello World", body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action); } [Theory] @@ -272,41 +59,9 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() [InlineData("bytes = 1-4, 5-11")] public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString) { - // Arrange - var contentType = "text/plain"; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes("Hello World"); - var readStream = new MemoryStream(byteArray); - - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = true, - }; - - var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - httpContext.Request.Headers.Range = rangeString; - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = streamReader.ReadToEndAsync().Result; - Assert.Empty(httpResponse.Headers.ContentRange); - Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); - Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - Assert.Equal("Hello World", body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(rangeString, action); } [Theory] @@ -314,132 +69,25 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore [InlineData("bytes = -0")] public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString) { - // Arrange - var contentType = "text/plain"; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes("Hello World"); - var readStream = new MemoryStream(byteArray); - - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = true, - }; - - var httpContext = GetHttpContext(); - httpContext.Request.Headers.Range = rangeString; - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Assert - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = streamReader.ReadToEndAsync().Result; - var contentRange = new ContentRangeHeaderValue(byteArray.Length); - Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode); - Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); - Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); - Assert.Equal(0, httpResponse.ContentLength); - Assert.Empty(body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(rangeString, action); } [Fact] public async Task WriteFileAsync_RangeRequested_PreconditionFailed() { - // Arrange - var contentType = "text/plain"; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes("Hello World"); - var readStream = new MemoryStream(byteArray); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = true, - }; - - var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfMatch = new[] - { - new EntityTagHeaderValue("\"NotEtag\""), - }; - httpContext.Request.Headers.Range = "bytes = 0-6"; - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = streamReader.ReadToEndAsync().Result; - Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode); - Assert.Null(httpResponse.ContentLength); - Assert.Empty(httpResponse.Headers.ContentRange); - Assert.NotEmpty(httpResponse.Headers.LastModified); - Assert.Empty(body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action); } [Fact] public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() { - // Arrange - var contentType = "text/plain"; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes("Hello World"); - var readStream = new MemoryStream(byteArray); - - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = true, - }; - - var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfNoneMatch = new[] - { - new EntityTagHeaderValue("\"Etag\""), - }; - httpContext.Request.Headers.Range = "bytes = 0-6"; - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = await streamReader.ReadToEndAsync(); - Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode); - Assert.Null(httpResponse.ContentLength); - Assert.Empty(httpResponse.Headers.ContentRange); - Assert.False(httpResponse.Headers.ContainsKey(HeaderNames.ContentType)); - Assert.NotEmpty(httpResponse.Headers.LastModified); - Assert.Empty(body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_NotModified_RangeRequestedIgnored(action); } [Theory] @@ -447,182 +95,41 @@ public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() [InlineData(null)] public async Task WriteFileAsync_RangeRequested_FileLengthZeroOrNull(long? fileLength) { - // Arrange - var contentType = "text/plain"; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - var byteArray = Encoding.ASCII.GetBytes(""); - var readStream = new MemoryStream(byteArray); - fileLength = fileLength ?? 0L; - readStream.SetLength(fileLength.Value); - var result = new FileStreamResult(readStream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = true, - }; - - var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.Range = new RangeHeaderValue(0, 5); - requestHeaders.IfMatch = new[] - { - new EntityTagHeaderValue("\"Etag\""), - }; - httpContext.Request.Method = HttpMethods.Get; - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - httpResponse.Body.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(httpResponse.Body); - var body = streamReader.ReadToEndAsync().Result; - Assert.Equal(lastModified.ToString("R"), httpResponse.Headers.LastModified); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - var contentRange = new ContentRangeHeaderValue(byteArray.Length); - Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode); - Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); - Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); - Assert.Equal(0, httpResponse.ContentLength); - Assert.Empty(body); - Assert.False(readStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_RangeRequested_FileLengthZeroOrNull(fileLength, action); } [Fact] public async Task WriteFileAsync_WritesResponse_InChunksOfFourKilobytes() { - // Arrange - var mockReadStream = new Mock(); - mockReadStream.SetupSequence(s => s.ReadAsync(It.IsAny(), 0, 0x1000, CancellationToken.None)) - .Returns(Task.FromResult(0x1000)) - .Returns(Task.FromResult(0x500)) - .Returns(Task.FromResult(0)); - - var mockBodyStream = new Mock(); - mockBodyStream - .Setup(s => s.WriteAsync(It.IsAny(), 0, 0x1000, CancellationToken.None)) - .Returns(Task.FromResult(0)); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - mockBodyStream - .Setup(s => s.WriteAsync(It.IsAny(), 0, 0x500, CancellationToken.None)) - .Returns(Task.FromResult(0)); - - var result = new FileStreamResult(mockReadStream.Object, "text/plain"); - - var httpContext = GetHttpContext(); - httpContext.Response.Body = mockBodyStream.Object; - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - mockReadStream.Verify(); - mockBodyStream.Verify(); + await BaseFileStreamResultTest.WriteFileAsync_WritesResponse_InChunksOfFourKilobytes(action); } [Fact] public async Task WriteFileAsync_CopiesProvidedStream_ToOutputStream() { - // Arrange - // Generate an array of bytes with a predictable pattern - // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13 - var originalBytes = Enumerable.Range(0, 0x1234) - .Select(b => (byte)(b % 20)).ToArray(); - - var originalStream = new MemoryStream(originalBytes); - - var httpContext = GetHttpContext(); - var outStream = new MemoryStream(); - httpContext.Response.Body = outStream; - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - var result = new FileStreamResult(originalStream, "text/plain"); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var outBytes = outStream.ToArray(); - Assert.True(originalBytes.SequenceEqual(outBytes)); - Assert.False(originalStream.CanSeek); + await BaseFileStreamResultTest.WriteFileAsync_CopiesProvidedStream_ToOutputStream(action); } [Fact] public async Task SetsSuppliedContentTypeAndEncoding() { - // Arrange - var expectedContentType = "text/foo; charset=us-ascii"; - // Generate an array of bytes with a predictable pattern - // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, 13 - var originalBytes = Enumerable.Range(0, 0x1234) - .Select(b => (byte)(b % 20)).ToArray(); - - var originalStream = new MemoryStream(originalBytes); - - var httpContext = GetHttpContext(); - var outStream = new MemoryStream(); - httpContext.Response.Body = outStream; - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - var result = new FileStreamResult(originalStream, expectedContentType); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var outBytes = outStream.ToArray(); - Assert.True(originalBytes.SequenceEqual(outBytes)); - Assert.Equal(expectedContentType, httpContext.Response.ContentType); - Assert.False(originalStream.CanSeek); + await BaseFileStreamResultTest.SetsSuppliedContentTypeAndEncoding(action); } [Fact] public async Task HeadRequest_DoesNotWriteToBody_AndClosesReadStream() { - // Arrange - var readStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello, World!")); - - var httpContext = GetHttpContext(); - httpContext.Request.Method = "HEAD"; - var outStream = new MemoryStream(); - httpContext.Response.Body = outStream; - - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - var result = new FileStreamResult(readStream, "text/plain"); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - Assert.False(readStream.CanSeek); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.Equal(0, httpContext.Response.Body.Length); - } - - private static IServiceCollection CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton, FileStreamResultExecutor>(); - services.AddSingleton(NullLoggerFactory.Instance); - return services; - } - - private static HttpContext GetHttpContext() - { - var services = CreateServices(); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = services.BuildServiceProvider(); - return httpContext; + await BaseFileStreamResultTest.HeadRequest_DoesNotWriteToBody_AndClosesReadStream(action); } } -} \ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/test/LocalRedirectActionResult.cs b/src/Mvc/Mvc.Core/test/LocalRedirectActionResult.cs new file mode 100644 index 000000000000..2b906102cbef --- /dev/null +++ b/src/Mvc/Mvc.Core/test/LocalRedirectActionResult.cs @@ -0,0 +1,95 @@ +// 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.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class LocalRedirectActionResultTest + { + [Fact] + public void Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new LocalRedirectResult(url); + + // Assert + Assert.False(result.PreserveMethod); + Assert.False(result.Permanent); + Assert.Same(url, result.Url); + } + + [Fact] + public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new LocalRedirectResult(url, permanent: true); + + // Assert + Assert.False(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.Same(url, result.Url); + } + + [Fact] + public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlPermanentAndPreserveMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new LocalRedirectResult(url, permanent: true, preserveMethod: true); + + // Assert + Assert.True(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.Same(url, result.Url); + } + + [Fact] + public async Task Execute_ReturnsExpectedValues() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseLocalRedirectResultTest.Execute_ReturnsExpectedValues(action); + } + + [Theory] + [InlineData("", "//")] + [InlineData("", "/\\")] + [InlineData("", "//foo")] + [InlineData("", "/\\foo")] + [InlineData("", "Home/About")] + [InlineData("/myapproot", "http://www.example.com")] + public async Task Execute_Throws_ForNonLocalUrl( + string appRoot, + string contentPath) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseLocalRedirectResultTest.Execute_Throws_ForNonLocalUrl(appRoot, contentPath, action); + } + + [Theory] + [InlineData("", "~//")] + [InlineData("", "~/\\")] + [InlineData("", "~//foo")] + [InlineData("", "~/\\foo")] + public async Task Execute_Throws_ForNonLocalUrlTilde( + string appRoot, + string contentPath) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseLocalRedirectResultTest.Execute_Throws_ForNonLocalUrlTilde(appRoot, contentPath, action); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/LocalRedirectResultTest.cs b/src/Mvc/Mvc.Core/test/LocalRedirectResultTest.cs index 5c90a65e87aa..507b2cf84bfe 100644 --- a/src/Mvc/Mvc.Core/test/LocalRedirectResultTest.cs +++ b/src/Mvc/Mvc.Core/test/LocalRedirectResultTest.cs @@ -4,84 +4,18 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc { public class LocalRedirectResultTest { - [Fact] - public void Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new LocalRedirectResult(url); - - // Assert - Assert.False(result.PreserveMethod); - Assert.False(result.Permanent); - Assert.Same(url, result.Url); - } - - [Fact] - public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new LocalRedirectResult(url, permanent: true); - - // Assert - Assert.False(result.PreserveMethod); - Assert.True(result.Permanent); - Assert.Same(url, result.Url); - } - - [Fact] - public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlPermanentAndPreserveMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new LocalRedirectResult(url, permanent: true, preserveMethod: true); - - // Assert - Assert.True(result.PreserveMethod); - Assert.True(result.Permanent); - Assert.Same(url, result.Url); - } - [Fact] public async Task Execute_ReturnsExpectedValues() { - // Arrange - var appRoot = "/"; - var contentPath = "~/Home/About"; - var expectedPath = "/Home/About"; - - var httpContext = GetHttpContext(appRoot); - var actionContext = GetActionContext(httpContext); - var result = new LocalRedirectResult(contentPath); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString()); - Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + await BaseLocalRedirectResultTest.Execute_ReturnsExpectedValues(action); } [Theory] @@ -95,17 +29,9 @@ public async Task Execute_Throws_ForNonLocalUrl( string appRoot, string contentPath) { - // Arrange - var httpContext = GetHttpContext(appRoot); - var actionContext = GetActionContext(httpContext); - var result = new LocalRedirectResult(contentPath); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Act & Assert - var exception = await Assert.ThrowsAsync(() => result.ExecuteResultAsync(actionContext)); - Assert.Equal( - "The supplied URL is not local. A URL with an absolute path is considered local if it does not " + - "have a host/authority part. URLs using virtual paths ('~/') are also local.", - exception.Message); + await BaseLocalRedirectResultTest.Execute_Throws_ForNonLocalUrl(appRoot, contentPath, action); } [Theory] @@ -117,43 +43,9 @@ public async Task Execute_Throws_ForNonLocalUrlTilde( string appRoot, string contentPath) { - // Arrange - var httpContext = GetHttpContext(appRoot); - var actionContext = GetActionContext(httpContext); - var result = new LocalRedirectResult(contentPath); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => result.ExecuteResultAsync(actionContext)); - Assert.Equal( - "The supplied URL is not local. A URL with an absolute path is considered local if it does not " + - "have a host/authority part. URLs using virtual paths ('~/') are also local.", - exception.Message); - } + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - private static ActionContext GetActionContext(HttpContext httpContext) - { - var routeData = new RouteData(); - routeData.Routers.Add(new Mock().Object); - - return new ActionContext(httpContext, routeData, new ActionDescriptor()); - } - - private static IServiceProvider GetServiceProvider() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton, LocalRedirectResultExecutor>(); - serviceCollection.AddSingleton(); - serviceCollection.AddTransient(); - return serviceCollection.BuildServiceProvider(); - } - - private static HttpContext GetHttpContext( - string appRoot) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = GetServiceProvider(); - httpContext.Request.PathBase = new PathString(appRoot); - return httpContext; + await BaseLocalRedirectResultTest.Execute_Throws_ForNonLocalUrlTilde(appRoot, contentPath, action); } } -} \ No newline at end of file +}