diff --git a/src/Mvc/Mvc.Core/src/FileContentResult.cs b/src/Mvc/Mvc.Core/src/FileContentResult.cs index 7fcb8729112f..f9bc761110b6 100644 --- a/src/Mvc/Mvc.Core/src/FileContentResult.cs +++ b/src/Mvc/Mvc.Core/src/FileContentResult.cs @@ -3,9 +3,12 @@ using System; 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 @@ -14,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc /// Represents an that when executed will /// write a binary file to the response. /// - public class FileContentResult : FileResult + public class FileContentResult : FileResult, IResult { private byte[] _fileContents; @@ -77,5 +80,43 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = context.HttpContext.RequestServices.GetRequiredService>(); return executor.ExecuteAsync(context, this); } + + Task IResult.ExecuteAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + + var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog( + httpContext, + this, + FileContents.Length, + EnableRangeProcessing, + LastModified, + EntityTag, + logger); + + if (!serveBody) + { + return Task.CompletedTask; + } + + if (range != null && rangeLength == 0) + { + return Task.CompletedTask; + } + + if (range != null) + { + logger.WritingRangeToBody(); + } + + var fileContentStream = new MemoryStream(FileContents); + return FileResultExecutorBase.WriteFileAsyncInternal(httpContext, fileContentStream, range, rangeLength); + } } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs b/src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs index c97c01cbf2ff..72dca6e2c2b5 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/FileResultExecutorBase.cs @@ -68,16 +68,35 @@ protected virtual (RangeItemHeaderValue? range, long rangeLength, bool serveBody DateTimeOffset? lastModified = null, EntityTagHeaderValue? etag = null) { - if (context == null) + return SetHeadersAndLog( + context.HttpContext, + result, + fileLength, + enableRangeProcessing, + lastModified, + etag, + Logger); + } + + internal static (RangeItemHeaderValue? range, long rangeLength, bool serveBody) SetHeadersAndLog( + HttpContext httpContext, + FileResult result, + long? fileLength, + bool enableRangeProcessing, + DateTimeOffset? lastModified, + EntityTagHeaderValue? etag, + ILogger logger) + { + if (httpContext == null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(httpContext)); } if (result == null) { throw new ArgumentNullException(nameof(result)); } - - var request = context.HttpContext.Request; + + var request = httpContext.Request; var httpRequestHeaders = request.GetTypedHeaders(); // Since the 'Last-Modified' and other similar http date headers are rounded down to whole seconds, @@ -87,9 +106,9 @@ protected virtual (RangeItemHeaderValue? range, long rangeLength, bool serveBody lastModified = RoundDownToWholeSeconds(lastModified.Value); } - var preconditionState = GetPreconditionState(httpRequestHeaders, lastModified, etag); + var preconditionState = GetPreconditionState(httpRequestHeaders, lastModified, etag, logger); - var response = context.HttpContext.Response; + var response = httpContext.Response; SetLastModifiedAndEtagHeaders(response, lastModified, etag); // Short circuit if the preconditional headers process to 304 (NotModified) or 412 (PreconditionFailed) @@ -104,8 +123,8 @@ protected virtual (RangeItemHeaderValue? range, long rangeLength, bool serveBody return (range: null, rangeLength: 0, serveBody: false); } - SetContentType(context, result); - SetContentDispositionHeader(context, result); + SetContentType(httpContext, result); + SetContentDispositionHeader(httpContext, result); if (fileLength.HasValue) { @@ -125,27 +144,27 @@ protected virtual (RangeItemHeaderValue? range, long rangeLength, bool serveBody // range should be processed and Range headers should be set if ((HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method)) && (preconditionState == PreconditionState.Unspecified || preconditionState == PreconditionState.ShouldProcess) - && (IfRangeValid(httpRequestHeaders, lastModified, etag))) + && (IfRangeValid(httpRequestHeaders, lastModified, etag, logger))) { - return SetRangeHeaders(context, httpRequestHeaders, fileLength.Value); + return SetRangeHeaders(httpContext, httpRequestHeaders, fileLength.Value, logger); } } else { - Logger.NotEnabledForRangeProcessing(); + logger.NotEnabledForRangeProcessing(); } } return (range: null, rangeLength: 0, serveBody: !HttpMethods.IsHead(request.Method)); } - private static void SetContentType(ActionContext context, FileResult result) + private static void SetContentType(HttpContext httpContext, FileResult result) { - var response = context.HttpContext.Response; + var response = httpContext.Response; response.ContentType = result.ContentType; } - private static void SetContentDispositionHeader(ActionContext context, FileResult result) + private static void SetContentDispositionHeader(HttpContext httpContext, FileResult result) { if (!string.IsNullOrEmpty(result.FileDownloadName)) { @@ -156,7 +175,7 @@ private static void SetContentDispositionHeader(ActionContext context, FileResul // basis for the actual filename, where possible. var contentDisposition = new ContentDispositionHeaderValue("attachment"); contentDisposition.SetHttpFileName(result.FileDownloadName); - context.HttpContext.Response.Headers.ContentDisposition = contentDisposition.ToString(); + httpContext.Response.Headers.ContentDisposition = contentDisposition.ToString(); } } @@ -178,10 +197,11 @@ private static void SetAcceptRangeHeader(HttpResponse response) response.Headers.AcceptRanges = AcceptRangeHeaderValue; } - internal bool IfRangeValid( + internal static bool IfRangeValid( RequestHeaders httpRequestHeaders, DateTimeOffset? lastModified, - EntityTagHeaderValue? etag) + EntityTagHeaderValue? etag, + ILogger logger) { // 14.27 If-Range var ifRange = httpRequestHeaders.IfRange; @@ -196,13 +216,13 @@ internal bool IfRangeValid( { if (lastModified.HasValue && lastModified > ifRange.LastModified) { - Logger.IfRangeLastModifiedPreconditionFailed(lastModified, ifRange.LastModified); + logger.IfRangeLastModifiedPreconditionFailed(lastModified, ifRange.LastModified); return false; } } else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true)) { - Logger.IfRangeETagPreconditionFailed(etag, ifRange.EntityTag); + logger.IfRangeETagPreconditionFailed(etag, ifRange.EntityTag); return false; } } @@ -211,10 +231,11 @@ internal bool IfRangeValid( } // Internal for testing - internal PreconditionState GetPreconditionState( + internal static PreconditionState GetPreconditionState( RequestHeaders httpRequestHeaders, DateTimeOffset? lastModified, - EntityTagHeaderValue? etag) + EntityTagHeaderValue? etag, + ILogger logger) { var ifMatchState = PreconditionState.Unspecified; var ifNoneMatchState = PreconditionState.Unspecified; @@ -234,7 +255,7 @@ internal PreconditionState GetPreconditionState( if (ifMatchState == PreconditionState.PreconditionFailed) { - Logger.IfMatchPreconditionFailed(etag); + logger.IfMatchPreconditionFailed(etag); } } @@ -269,7 +290,7 @@ internal PreconditionState GetPreconditionState( if (ifUnmodifiedSinceState == PreconditionState.PreconditionFailed) { - Logger.IfUnmodifiedSincePreconditionFailed(lastModified, ifUnmodifiedSince); + logger.IfUnmodifiedSincePreconditionFailed(lastModified, ifUnmodifiedSince); } } @@ -316,22 +337,23 @@ private static PreconditionState GetMaxPreconditionState(params PreconditionStat return max; } - private (RangeItemHeaderValue? range, long rangeLength, bool serveBody) SetRangeHeaders( - ActionContext context, + private static (RangeItemHeaderValue? range, long rangeLength, bool serveBody) SetRangeHeaders( + HttpContext httpContext, RequestHeaders httpRequestHeaders, - long fileLength) + long fileLength, + ILogger logger) { - var response = context.HttpContext.Response; + var response = httpContext.Response; var httpResponseHeaders = response.GetTypedHeaders(); - var serveBody = !HttpMethods.IsHead(context.HttpContext.Request.Method); + var serveBody = !HttpMethods.IsHead(httpContext.Request.Method); // Range may be null for empty range header, invalid ranges, parsing errors, multiple ranges // and when the file length is zero. var (isRangeRequest, range) = RangeHelper.ParseRange( - context.HttpContext, + httpContext, httpRequestHeaders, fileLength, - Logger); + logger); if (!isRangeRequest) { @@ -397,6 +419,11 @@ protected static ILogger CreateLogger(ILoggerFactory factory) /// The range length. /// The async task. protected static async Task WriteFileAsync(HttpContext context, Stream fileStream, RangeItemHeaderValue? range, long rangeLength) + { + await WriteFileAsyncInternal(context, fileStream, range, rangeLength); + } + + internal static async Task WriteFileAsyncInternal(HttpContext context, Stream fileStream, RangeItemHeaderValue? range, long rangeLength) { var outputStream = context.Response.Body; using (fileStream) diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/PhysicalFileResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/PhysicalFileResultExecutor.cs index 7d1b5cfa161d..f295d2769f77 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/PhysicalFileResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/PhysicalFileResultExecutor.cs @@ -3,10 +3,8 @@ using System; using System.IO; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -69,9 +67,19 @@ public virtual Task ExecuteAsync(ActionContext context, PhysicalFileResult resul /// protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength) { - if (context == null) + return WriteFileAsyncInternal(context.HttpContext, result, range, rangeLength, Logger); + } + + internal static Task WriteFileAsyncInternal( + HttpContext httpContext, + PhysicalFileResult result, + RangeItemHeaderValue? range, + long rangeLength, + ILogger logger) + { + if (httpContext == null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(httpContext)); } if (result == null) @@ -84,7 +92,7 @@ protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult return Task.CompletedTask; } - var response = context.HttpContext.Response; + var response = httpContext.Response; if (!Path.IsPathRooted(result.FileName)) { throw new NotSupportedException(Resources.FormatFileResult_PathNotRooted(result.FileName)); @@ -92,7 +100,7 @@ protected virtual Task WriteFileAsync(ActionContext context, PhysicalFileResult if (range != null) { - Logger.WritingRangeToBody(); + logger.WritingRangeToBody(); } if (range != null) diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs index 68e5da333b4c..3ec1178cda2d 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs @@ -50,7 +50,7 @@ public virtual Task ExecuteAsync(ActionContext context, VirtualFileResult result throw new ArgumentNullException(nameof(result)); } - var fileInfo = GetFileInformation(result); + var fileInfo = GetFileInformation(result, _hostingEnvironment); if (!fileInfo.Exists) { throw new FileNotFoundException( @@ -89,16 +89,26 @@ protected virtual Task WriteFileAsync(ActionContext context, VirtualFileResult r throw new ArgumentNullException(nameof(result)); } + return WriteFileAsyncInternal(context.HttpContext, fileInfo, range, rangeLength, Logger); + } + + internal static Task WriteFileAsyncInternal( + HttpContext httpContext, + IFileInfo fileInfo, + RangeItemHeaderValue? range, + long rangeLength, + ILogger logger) + { if (range != null && rangeLength == 0) { return Task.CompletedTask; } - var response = context.HttpContext.Response; + var response = httpContext.Response; if (range != null) { - Logger.WritingRangeToBody(); + logger.WritingRangeToBody(); } if (range != null) @@ -113,9 +123,9 @@ protected virtual Task WriteFileAsync(ActionContext context, VirtualFileResult r count: null); } - private IFileInfo GetFileInformation(VirtualFileResult result) + internal static IFileInfo GetFileInformation(VirtualFileResult result, IWebHostEnvironment hostingEnvironment) { - var fileProvider = GetFileProvider(result); + var fileProvider = GetFileProvider(result, hostingEnvironment); if (fileProvider is NullFileProvider) { throw new InvalidOperationException(Resources.VirtualFileResultExecutor_NoFileProviderConfigured); @@ -131,14 +141,14 @@ private IFileInfo GetFileInformation(VirtualFileResult result) return fileInfo; } - private IFileProvider GetFileProvider(VirtualFileResult result) + internal static IFileProvider GetFileProvider(VirtualFileResult result, IWebHostEnvironment hostingEnvironment) { if (result.FileProvider != null) { return result.FileProvider; } - result.FileProvider = _hostingEnvironment.WebRootFileProvider; + result.FileProvider = hostingEnvironment.WebRootFileProvider; return result.FileProvider; } diff --git a/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs b/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs index def6a2b65408..9734ddbacdfe 100644 --- a/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs +++ b/src/Mvc/Mvc.Core/src/PhysicalFileResult.cs @@ -3,9 +3,13 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc @@ -14,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc /// A on execution will write a file from disk to the response /// using mechanisms provided by the host. /// - public class PhysicalFileResult : FileResult + public class PhysicalFileResult : FileResult, IResult { private string _fileName; @@ -66,5 +70,51 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = context.HttpContext.RequestServices.GetRequiredService>(); return executor.ExecuteAsync(context, this); } + + Task IResult.ExecuteAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var fileInfo = new FileInfo(FileName); + if (!fileInfo.Exists) + { + throw new FileNotFoundException( + Resources.FormatFileResult_InvalidPath(FileName), FileName); + } + + return ExecuteAsyncInternal(httpContext, this, fileInfo.LastWriteTimeUtc, fileInfo.Length); + } + + internal static Task ExecuteAsyncInternal( + HttpContext httpContext, + PhysicalFileResult result, + DateTimeOffset fileLastModified, + long fileLength) + { + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + + logger.ExecutingFileResult(result, result.FileName); + + var lastModified = result.LastModified ?? fileLastModified; + var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog( + httpContext, + result, + fileLength, + result.EnableRangeProcessing, + lastModified, + result.EntityTag, + logger); + + if (serveBody) + { + return PhysicalFileResultExecutor.WriteFileAsyncInternal(httpContext, result, range, rangeLength, logger); + } + + return Task.CompletedTask; + } } } diff --git a/src/Mvc/Mvc.Core/src/RedirectResult.cs b/src/Mvc/Mvc.Core/src/RedirectResult.cs index 22db7fc57ed1..8ce2beb6f6b4 100644 --- a/src/Mvc/Mvc.Core/src/RedirectResult.cs +++ b/src/Mvc/Mvc.Core/src/RedirectResult.cs @@ -4,10 +4,13 @@ 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.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc { @@ -15,7 +18,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 URL. /// - public class RedirectResult : ActionResult, IKeepTempDataResult + public class RedirectResult : ActionResult, IResult, IKeepTempDataResult { private string _url; @@ -112,5 +115,36 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = context.HttpContext.RequestServices.GetRequiredService>(); return executor.ExecuteAsync(context, this); } + + /// + Task IResult.ExecuteAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + + // IsLocalUrl is called to handle URLs starting with '~/'. + var destinationUrl = UrlHelperBase.CheckIsLocalUrl(_url) ? UrlHelperBase.Content(httpContext, _url) : _url; + + logger.RedirectResultExecuting(destinationUrl); + + if (PreserveMethod) + { + httpContext.Response.StatusCode = Permanent + ? StatusCodes.Status308PermanentRedirect + : StatusCodes.Status307TemporaryRedirect; + httpContext.Response.Headers.Location = destinationUrl; + } + else + { + httpContext.Response.Redirect(destinationUrl, Permanent); + } + + return Task.CompletedTask; + } } } diff --git a/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs b/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs index 92a4280446ef..44717c66a27f 100644 --- a/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs +++ b/src/Mvc/Mvc.Core/src/Routing/UrlHelperBase.cs @@ -49,86 +49,11 @@ protected UrlHelperBase(ActionContext actionContext) public ActionContext ActionContext { get; } /// - public virtual bool IsLocalUrl([NotNullWhen(true)] string? url) - { - if (string.IsNullOrEmpty(url)) - { - return false; - } - - // Allows "/" or "/foo" but not "//" or "/\". - if (url[0] == '/') - { - // url is exactly "/" - if (url.Length == 1) - { - return true; - } - - // url doesn't start with "//" or "/\" - if (url[1] != '/' && url[1] != '\\') - { - return !HasControlCharacter(url.AsSpan(1)); - } - - return false; - } - - // Allows "~/" or "~/foo" but not "~//" or "~/\". - if (url[0] == '~' && url.Length > 1 && url[1] == '/') - { - // url is exactly "~/" - if (url.Length == 2) - { - return true; - } - - // url doesn't start with "~//" or "~/\" - if (url[2] != '/' && url[2] != '\\') - { - return !HasControlCharacter(url.AsSpan(2)); - } - - return false; - } - - return false; - - static bool HasControlCharacter(ReadOnlySpan readOnlySpan) - { - // URLs may not contain ASCII control characters. - for (var i = 0; i < readOnlySpan.Length; i++) - { - if (char.IsControl(readOnlySpan[i])) - { - return true; - } - } - - return false; - } - } + public virtual bool IsLocalUrl([NotNullWhen(true)] string? url) => CheckIsLocalUrl(url); /// [return: NotNullIfNotNull("contentPath")] - public virtual string? Content(string? contentPath) - { - if (string.IsNullOrEmpty(contentPath)) - { - return null; - } - else if (contentPath[0] == '~') - { - var segment = new PathString(contentPath.Substring(1)); - var applicationPath = ActionContext.HttpContext.Request.PathBase; - - var path = applicationPath.Add(segment); - Debug.Assert(path.HasValue); - return path.Value; - } - - return contentPath; - } + public virtual string? Content(string? contentPath) => Content(ActionContext.HttpContext, contentPath); /// public virtual string? Link(string? routeName, object? values) @@ -372,6 +297,86 @@ internal static void NormalizeRouteValuesForPage( } } + [return: NotNullIfNotNull("contentPath")] + internal static string? Content(HttpContext httpContext, string? contentPath) + { + if (string.IsNullOrEmpty(contentPath)) + { + return null; + } + else if (contentPath[0] == '~') + { + var segment = new PathString(contentPath.Substring(1)); + var applicationPath = httpContext.Request.PathBase; + + var path = applicationPath.Add(segment); + Debug.Assert(path.HasValue); + return path.Value; + } + + return contentPath; + } + + internal static bool CheckIsLocalUrl([NotNullWhen(true)] string? url) + { + if (string.IsNullOrEmpty(url)) + { + return false; + } + + // Allows "/" or "/foo" but not "//" or "/\". + if (url[0] == '/') + { + // url is exactly "/" + if (url.Length == 1) + { + return true; + } + + // url doesn't start with "//" or "/\" + if (url[1] != '/' && url[1] != '\\') + { + return !HasControlCharacter(url.AsSpan(1)); + } + + return false; + } + + // Allows "~/" or "~/foo" but not "~//" or "~/\". + if (url[0] == '~' && url.Length > 1 && url[1] == '/') + { + // url is exactly "~/" + if (url.Length == 2) + { + return true; + } + + // url doesn't start with "~//" or "~/\" + if (url[2] != '/' && url[2] != '\\') + { + return !HasControlCharacter(url.AsSpan(2)); + } + + return false; + } + + return false; + + static bool HasControlCharacter(ReadOnlySpan readOnlySpan) + { + // URLs may not contain ASCII control characters. + for (var i = 0; i < readOnlySpan.Length; i++) + { + if (char.IsControl(readOnlySpan[i])) + { + return true; + } + } + + return false; + } + } + private static object CalculatePageName(ActionContext? context, RouteValueDictionary? ambientValues, string pageName) { Debug.Assert(pageName.Length > 0); diff --git a/src/Mvc/Mvc.Core/src/VirtualFileResult.cs b/src/Mvc/Mvc.Core/src/VirtualFileResult.cs index 7d49d9de41de..ac95c84a07eb 100644 --- a/src/Mvc/Mvc.Core/src/VirtualFileResult.cs +++ b/src/Mvc/Mvc.Core/src/VirtualFileResult.cs @@ -3,10 +3,15 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc @@ -15,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc /// A that on execution writes the file specified using a virtual path to the response /// using mechanisms provided by the host. /// - public class VirtualFileResult : FileResult + public class VirtualFileResult : FileResult, IResult { private string _fileName; @@ -69,5 +74,42 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = context.HttpContext.RequestServices.GetRequiredService>(); return executor.ExecuteAsync(context, this); } + + Task IResult.ExecuteAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var hostingEnvironment = httpContext.RequestServices.GetRequiredService(); + + var fileInfo = VirtualFileResultExecutor.GetFileInformation(this, hostingEnvironment); + if (!fileInfo.Exists) + { + throw new FileNotFoundException( + Resources.FormatFileResult_InvalidPath(FileName), FileName); + } + + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + + var lastModified = LastModified ?? fileInfo.LastModified; + var (range, rangeLength, serveBody) = FileResultExecutorBase.SetHeadersAndLog( + httpContext, + this, + fileInfo.Length, + EnableRangeProcessing, + lastModified, + EntityTag, + logger); + + if (serveBody) + { + return VirtualFileResultExecutor.WriteFileAsyncInternal(httpContext, fileInfo, range, rangeLength, logger); + } + + return Task.CompletedTask; + } } } diff --git a/src/Mvc/Mvc.Core/test/FileContentResultTest.cs b/src/Mvc/Mvc.Core/test/BaseFileContentResultTest.cs similarity index 80% rename from src/Mvc/Mvc.Core/test/FileContentResultTest.cs rename to src/Mvc/Mvc.Core/test/BaseFileContentResultTest.cs index 77380a55755c..cb397e816a81 100644 --- a/src/Mvc/Mvc.Core/test/FileContentResultTest.cs +++ b/src/Mvc/Mvc.Core/test/BaseFileContentResultTest.cs @@ -17,62 +17,10 @@ namespace Microsoft.AspNetCore.Mvc { - public class FileContentResultTest + public class BaseFileContentResultTest { - [Fact] - public void Constructor_SetsFileContents() - { - // Arrange - var fileContents = new byte[0]; - - // Act - var result = new FileContentResult(fileContents, "text/plain"); - - // Assert - Assert.Same(fileContents, result.FileContents); - } - - [Fact] - public void Constructor_SetsContentTypeAndParameters() - { - // Arrange - var fileContents = new byte[0]; - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var expectedMediaType = contentType; - - // Act - var result = new FileContentResult(fileContents, contentType); - - // Assert - Assert.Same(fileContents, result.FileContents); - MediaTypeAssert.Equal(expectedMediaType, result.ContentType); - } - - [Fact] - public void Constructor_SetsLastModifiedAndEtag() - { - // Arrange - var fileContents = new byte[0]; - var contentType = "text/plain"; - var expectedMediaType = contentType; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); - - // Act - var result = new FileContentResult(fileContents, contentType) - { - LastModified = lastModified, - EntityTag = entityTag - }; - - // Assert - Assert.Equal(lastModified, result.LastModified); - Assert.Equal(entityTag, result.EntityTag); - MediaTypeAssert.Equal(expectedMediaType, result.ContentType); - } - - [Fact] - public async Task WriteFileAsync_CopiesBuffer_ToOutputStream() + public static async Task WriteFileAsync_CopiesBuffer_ToOutputStream( + Func function) { // Arrange var buffer = new byte[] { 1, 2, 3, 4, 5 }; @@ -82,23 +30,24 @@ public async Task WriteFileAsync_CopiesBuffer_ToOutputStream() var outStream = new MemoryStream(); httpContext.Response.Body = outStream; - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); var result = new FileContentResult(buffer, "text/plain"); // Act - await result.ExecuteResultAsync(context); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert Assert.Equal(buffer, outStream.ToArray()); } - [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) + public static async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested( + long? start, + long? end, + string expectedString, + long contentLength, + Func function) { // Arrange var contentType = "text/plain"; @@ -125,7 +74,8 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert start = start ?? 11 - end; @@ -144,8 +94,8 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque Assert.Equal(expectedString, body); } - [Fact] - public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest() + public static async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest( + Func function) { // Arrange var contentType = "text/plain"; @@ -173,7 +123,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -200,8 +151,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest() } } - [Fact] - public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored() + public static async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored( + Func function) { // Arrange var contentType = "text/plain"; @@ -228,7 +179,8 @@ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -241,8 +193,8 @@ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored() Assert.Equal("Hello World", body); } - [Fact] - public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored() + public static async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored( + Func function) { // Arrange var contentType = "text/plain"; @@ -270,7 +222,8 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -283,11 +236,9 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored() Assert.Equal("Hello World", body); } - [Theory] - [InlineData("0-5")] - [InlineData("bytes = ")] - [InlineData("bytes = 1-4, 5-11")] - public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString) + public static async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored( + string rangeString, + Func function) { // Arrange var contentType = "text/plain"; @@ -309,7 +260,8 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -323,10 +275,9 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnore Assert.Equal("Hello World", body); } - [Theory] - [InlineData("bytes = 12-13")] - [InlineData("bytes = -0")] - public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString) + public static async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable( + string rangeString, + Func function) { // Arrange var contentType = "text/plain"; @@ -348,7 +299,8 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotS var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -365,8 +317,8 @@ public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotS Assert.Empty(body); } - [Fact] - public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored() + public static async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored( + Func function) { // Arrange var contentType = "text/plain"; @@ -393,7 +345,8 @@ public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -407,8 +360,8 @@ public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored() Assert.Empty(body); } - [Fact] - public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() + public static async Task WriteFileAsync_NotModified_RangeRequestedIgnored( + Func function) { // Arrange var contentType = "text/plain"; @@ -435,7 +388,8 @@ public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -450,8 +404,8 @@ public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() Assert.Empty(body); } - [Fact] - public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + public static async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding( + Func function) { // Arrange var expectedContentType = "text/foo; charset=us-ascii"; @@ -462,12 +416,13 @@ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() var outStream = new MemoryStream(); httpContext.Response.Body = outStream; - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); var result = new FileContentResult(buffer, expectedContentType); // Act - await result.ExecuteResultAsync(context); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert Assert.Equal(buffer, outStream.ToArray()); @@ -493,4 +448,4 @@ private static HttpContext GetHttpContext() return httpContext; } } -} \ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/test/PhysicalFileResultTest.cs b/src/Mvc/Mvc.Core/test/BasePhysicalFileResultTest.cs similarity index 75% rename from src/Mvc/Mvc.Core/test/PhysicalFileResultTest.cs rename to src/Mvc/Mvc.Core/test/BasePhysicalFileResultTest.cs index 8d29080dd204..c4d8960e879e 100644 --- a/src/Mvc/Mvc.Core/test/PhysicalFileResultTest.cs +++ b/src/Mvc/Mvc.Core/test/BasePhysicalFileResultTest.cs @@ -2,9 +2,9 @@ // 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 System.IO.Pipelines; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -21,43 +21,14 @@ namespace Microsoft.AspNetCore.Mvc { - public class PhysicalFileResultTest + public class BasePhysicalFileResultTest { - [Fact] - public void Constructor_SetsFileName() - { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - - // Act - var result = new TestPhysicalFileResult(path, "text/plain"); - - // Assert - Assert.Equal(path, result.FileName); - } - - [Fact] - public void Constructor_SetsContentTypeAndParameters() - { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var expectedMediaType = contentType; - - // Act - var result = new TestPhysicalFileResult(path, contentType); - - // Assert - Assert.Equal(path, result.FileName); - MediaTypeAssert.Equal(expectedMediaType, result.ContentType); - } - - [Theory] - [InlineData(0, 3, "File", 4)] - [InlineData(8, 13, "Result", 6)] - [InlineData(null, 5, "ts�", 5)] - [InlineData(8, null, "ResultTestFile contents�", 26)] - public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength) + public static async Task WriteFileAsync_WritesRangeRequested( + long? start, + long? end, + string expectedString, + long contentLength, + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -73,7 +44,8 @@ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, st var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var startResult = start ?? 34 - end; @@ -90,8 +62,8 @@ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, st Assert.Equal((long?)contentLength, sendFile.Length); } - [Fact] - public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() + public static async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange( + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -109,7 +81,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -125,8 +98,8 @@ public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() Assert.Equal(4, sendFile.Length); } - [Fact] - public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored() + public static async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored( + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -143,7 +116,8 @@ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -154,8 +128,8 @@ public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored Assert.Null(sendFile.Length); } - [Fact] - public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() + public static async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored( + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -173,7 +147,8 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -184,11 +159,9 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() Assert.Null(sendFile.Length); } - [Theory] - [InlineData("0-5")] - [InlineData("bytes = ")] - [InlineData("bytes = 1-4, 5-11")] - public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString) + public static async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored( + string rangeString, + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -203,7 +176,8 @@ public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -215,10 +189,9 @@ public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string Assert.Null(sendFile.Length); } - [Theory] - [InlineData("bytes = 35-36")] - [InlineData("bytes = -0")] - public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString) + public static async Task WriteFileAsync_RangeRequestedNotSatisfiable( + string rangeString, + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -233,7 +206,8 @@ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -249,8 +223,8 @@ public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString Assert.Empty(body); } - [Fact] - public async Task WriteFileAsync_RangeRequested_PreconditionFailed() + public static async Task WriteFileAsync_RangeRequested_PreconditionFailed( + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -265,7 +239,8 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -279,8 +254,8 @@ public async Task WriteFileAsync_RangeRequested_PreconditionFailed() Assert.Empty(body); } - [Fact] - public async Task WriteFileAsync_RangeRequested_NotModified() + public static async Task WriteFileAsync_RangeRequested_NotModified( + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -295,7 +270,8 @@ public async Task WriteFileAsync_RangeRequested_NotModified() var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert var httpResponse = actionContext.HttpContext.Response; @@ -310,8 +286,8 @@ public async Task WriteFileAsync_RangeRequested_NotModified() Assert.Empty(body); } - [Fact] - public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() + public static async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent( + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -323,21 +299,21 @@ public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() var httpContext = GetHttpContext(); httpContext.Features.Set(sendFileMock.Object); - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(context); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert sendFileMock.Verify(); } - [Theory] - [InlineData(0, 3, 4)] - [InlineData(8, 13, 6)] - [InlineData(null, 3, 3)] - [InlineData(8, null, 26)] - public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, long contentLength) + public static async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent( + long? start, + long? end, + long contentLength, + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); @@ -354,7 +330,8 @@ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHtt var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(actionContext); + object functionContext = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)functionContext); // Assert start = start ?? 34 - end; @@ -372,8 +349,8 @@ public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHtt Assert.Equal(contentLength, httpResponse.ContentLength); } - [Fact] - public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + public static async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding( + Func function) { // Arrange var expectedContentType = "text/foo; charset=us-ascii"; @@ -382,10 +359,11 @@ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() var sendFile = new TestSendFileFeature(); var httpContext = GetHttpContext(); httpContext.Features.Set(sendFile); - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(context); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert Assert.Equal(expectedContentType, httpContext.Response.ContentType); @@ -395,8 +373,8 @@ public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() Assert.Equal(CancellationToken.None, sendFile.Token); } - [Fact] - public async Task ExecuteResultAsync_WorksWithAbsolutePaths() + public static async Task ExecuteResultAsync_WorksWithAbsolutePaths( + Func function) { // Arrange var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt")); @@ -406,10 +384,11 @@ public async Task ExecuteResultAsync_WorksWithAbsolutePaths() var httpContext = GetHttpContext(); httpContext.Features.Set(sendFile); - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act - await result.ExecuteResultAsync(context); + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); // Assert Assert.Equal(Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt")), sendFile.Name); @@ -418,66 +397,53 @@ public async Task ExecuteResultAsync_WorksWithAbsolutePaths() Assert.Equal(CancellationToken.None, sendFile.Token); } - [Theory] - [InlineData("FilePathResultTestFile.txt")] - [InlineData("./FilePathResultTestFile.txt")] - [InlineData(".\\FilePathResultTestFile.txt")] - [InlineData("~/FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles/FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")] - [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")] - [InlineData("~/SubFolder/SubFolderTestFile.txt")] - [InlineData("~/SubFolder\\SubFolderTestFile.txt")] - public async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(string path) + public static async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths( + string path, + Func function) { // Arrange var result = new TestPhysicalFileResult(path, "text/plain"); - var context = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor()); var expectedMessage = $"Path '{path}' was not rooted."; // Act - var ex = await Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); + object context = typeof(TContext) == typeof(HttpContext) ? actionContext.HttpContext : actionContext; + var ex = await Assert.ThrowsAsync( + () => function(result, (TContext)context)); // Assert Assert.Equal(expectedMessage, ex.Message); } - [Theory] - [InlineData("/SubFolder/SubFolderTestFile.txt")] - [InlineData("\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("/SubFolder\\SubFolderTestFile.txt")] - [InlineData("\\SubFolder/SubFolderTestFile.txt")] - [InlineData("./SubFolder/SubFolderTestFile.txt")] - [InlineData(".\\SubFolder\\SubFolderTestFile.txt")] - [InlineData("./SubFolder\\SubFolderTestFile.txt")] - [InlineData(".\\SubFolder/SubFolderTestFile.txt")] - public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path) + public static void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths( + string path, + Func function) { // Arrange var result = new TestPhysicalFileResult(path, "text/plain"); - var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); // Act & Assert - Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); + object context = typeof(TContext) == typeof(HttpContext) ? actionContext.HttpContext : actionContext; + Assert.ThrowsAsync( + () => function(result, (TContext)context)); } - [Theory] - [InlineData("/FilePathResultTestFile.txt")] - [InlineData("\\FilePathResultTestFile.txt")] - public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path) + public static void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths( + string path, + Func function) { // Arrange var result = new TestPhysicalFileResult(path, "text/plain"); - var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); // Act & Assert - Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); + object context = typeof(TContext) == typeof(HttpContext) ? actionContext.HttpContext : actionContext; + Assert.ThrowsAsync( + () => function(result, (TContext)context)); } - private class TestPhysicalFileResult : PhysicalFileResult + private class TestPhysicalFileResult : PhysicalFileResult, IResult { public TestPhysicalFileResult(string filePath, string contentType) : base(filePath, contentType) @@ -489,6 +455,13 @@ public override Task ExecuteResultAsync(ActionContext context) var executor = context.HttpContext.RequestServices.GetRequiredService(); return executor.ExecuteAsync(context, this); } + + Task IResult.ExecuteAsync(HttpContext httpContext) + { + var lastModified = DateTimeOffset.MinValue.AddDays(1); + var fileLastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0)); + return ExecuteAsyncInternal(httpContext, this, fileLastModified, 34); + } } private class TestPhysicalFileResultExecutor : PhysicalFileResultExecutor @@ -564,4 +537,4 @@ private static HttpContext GetHttpContext() return httpContext; } } -} \ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/test/BaseRedirectResultTest.cs b/src/Mvc/Mvc.Core/test/BaseRedirectResultTest.cs new file mode 100644 index 000000000000..6097a09263ec --- /dev/null +++ b/src/Mvc/Mvc.Core/test/BaseRedirectResultTest.cs @@ -0,0 +1,91 @@ +// 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 BaseRedirectResultTest + { + public static async Task Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde( + string appRoot, + string contentPath, + string expectedPath, + Func function) + { + // Arrange + var httpContext = GetHttpContext(appRoot); + var actionContext = GetActionContext(httpContext); + var result = new RedirectResult(contentPath); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext) context); + + // Assert + // Verifying if Redirect was called with the specific Url and parameter flag. + Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString()); + Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + } + + public static async Task Execute_ReturnsAppRelativePath_WhenItStartsWithTilde( + string appRoot, + string contentPath, + string expectedPath, + Func function) + { + // Arrange + var httpContext = GetHttpContext(appRoot); + var actionContext = GetActionContext(httpContext); + var result = new RedirectResult(contentPath); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + // Verifying if Redirect was called with the specific Url and parameter flag. + Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString()); + Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + } + + private static ActionContext GetActionContext(HttpContext httpContext) + { + var routeData = new RouteData(); + routeData.Routers.Add(Mock.Of()); + + return new ActionContext( + httpContext, + routeData, + new ActionDescriptor()); + } + + private static IServiceProvider GetServiceProvider() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton, RedirectResultExecutor>(); + 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/BaseVirtualFileResultTest.cs b/src/Mvc/Mvc.Core/test/BaseVirtualFileResultTest.cs new file mode 100644 index 000000000000..727d18eadb8f --- /dev/null +++ b/src/Mvc/Mvc.Core/test/BaseVirtualFileResultTest.cs @@ -0,0 +1,748 @@ +// 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.IO.Pipelines; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Net.Http.Headers; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class BaseVirtualFileResultTest + { + public static async Task WriteFileAsync_WritesRangeRequested( + long? start, + long? end, + string expectedString, + long contentLength, + Func function) + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.Range = new RangeHeaderValue(start, end); + requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1); + httpContext.Request.Method = HttpMethods.Get; + 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 startResult = start ?? 33 - end; + var endResult = startResult + contentLength - 1; + var httpResponse = actionContext.HttpContext.Response; + var contentRange = new ContentRangeHeaderValue(startResult.Value, endResult.Value, 33); + Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); + Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); + Assert.NotEmpty(httpResponse.Headers.LastModified); + Assert.Equal(contentLength, httpResponse.ContentLength); + Assert.Equal(path, sendFileFeature.Name); + Assert.Equal(startResult, sendFileFeature.Offset); + Assert.Equal((long?)contentLength, sendFileFeature.Length); + } + + public static async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange( + Func function) + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; + requestHeaders.Range = new RangeHeaderValue(0, 3); + requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); + httpContext.Request.Method = HttpMethods.Get; + 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; + Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); + var contentRange = new ContentRangeHeaderValue(0, 3, 33); + Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + Assert.Equal(4, httpResponse.ContentLength); + Assert.Equal(path, sendFileFeature.Name); + Assert.Equal(0, sendFileFeature.Offset); + Assert.Equal(4, sendFileFeature.Length); + } + + public static async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored( + Func function) + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var result = new TestVirtualFileResult(path, contentType); + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; + requestHeaders.Range = new RangeHeaderValue(0, 3); + requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); + httpContext.Request.Method = HttpMethods.Get; + 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; + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + Assert.Equal(path, sendFileFeature.Name); + Assert.Equal(0, sendFileFeature.Offset); + Assert.Null(sendFileFeature.Length); + } + + public static async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored( + Func function) + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; + requestHeaders.Range = new RangeHeaderValue(0, 3); + requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\"")); + httpContext.Request.Method = HttpMethods.Get; + 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; + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); + Assert.Equal(path, sendFileFeature.Name); + Assert.Equal(0, sendFileFeature.Offset); + Assert.Null(sendFileFeature.Length); + } + + public static async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored( + string rangeString, + Func function) + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var requestHeaders = httpContext.Request.GetTypedHeaders(); + httpContext.Request.Headers.Range = rangeString; + requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1); + httpContext.Request.Method = HttpMethods.Get; + 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; + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Empty(httpResponse.Headers.ContentRange); + Assert.NotEmpty(httpResponse.Headers.LastModified); + Assert.Equal(path, sendFileFeature.Name); + Assert.Equal(0, sendFileFeature.Offset); + Assert.Null(sendFileFeature.Length); + } + + public static async Task WriteFileAsync_RangeRequestedNotSatisfiable( + string rangeString, + Func function) + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var httpContext = GetHttpContext(); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var requestHeaders = httpContext.Request.GetTypedHeaders(); + httpContext.Request.Headers.Range = rangeString; + requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1); + 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(33); + Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); + Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); + Assert.NotEmpty(httpResponse.Headers.LastModified); + Assert.Equal(0, httpResponse.ContentLength); + Assert.Empty(body); + } + + public static async Task WriteFileAsync_RangeRequested_PreconditionFailed( + Func function) + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; + httpContext.Request.Headers.Range = "bytes = 0-6"; + httpContext.Request.Method = HttpMethods.Get; + 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; + Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode); + Assert.Null(httpResponse.ContentLength); + Assert.Empty(httpResponse.Headers.ContentRange); + Assert.NotEmpty(httpResponse.Headers.LastModified); + Assert.Null(sendFileFeature.Name); // Not called + } + + public static async Task WriteFileAsync_RangeRequested_NotModified( + Func function) + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var result = new TestVirtualFileResult(path, contentType); + result.EnableRangeProcessing = true; + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1); + httpContext.Request.Headers.Range = "bytes = 0-6"; + httpContext.Request.Method = HttpMethods.Get; + 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; + Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode); + Assert.Null(httpResponse.ContentLength); + Assert.Empty(httpResponse.Headers.ContentRange); + Assert.NotEmpty(httpResponse.Headers.LastModified); + Assert.False(httpResponse.Headers.ContainsKey(HeaderNames.ContentType)); + Assert.Null(sendFileFeature.Name); // Not called + } + + public static async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent( + Func function) + { + // Arrange + var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); + var result = new TestVirtualFileResult(path, "text/plain"); + + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + Assert.Equal(path, sendFileFeature.Name); + Assert.Equal(0, sendFileFeature.Offset); + Assert.Null(sendFileFeature.Length); + } + + public static async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent( + Func function) + { + // Arrange + var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); + var result = new TestVirtualFileResult(path, "text/plain") + { + FileProvider = GetFileProvider(path), + }; + + var sendFileMock = new Mock(); + sendFileMock + .Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None)) + .Returns(Task.FromResult(0)); + + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileMock.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 + sendFileMock.Verify(); + } + public static async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent( + long? start, + long? end, + string expectedString, + long contentLength, + Func function) + { + // Arrange + var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); + var result = new TestVirtualFileResult(path, "text/plain") + { + FileProvider = GetFileProvider(path), + EnableRangeProcessing = true, + }; + + var sendFile = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFile); + var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var appEnvironment = new Mock(); + appEnvironment.Setup(app => app.WebRootFileProvider) + .Returns(GetFileProvider(path)); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(appEnvironment.Object) + .AddTransient, TestVirtualFileResultExecutor>() + .AddTransient() + .BuildServiceProvider(); + + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.Range = new RangeHeaderValue(start, end); + requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1); + httpContext.Request.Method = HttpMethods.Get; + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object functionContext = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext) functionContext); + + // Assert + start = start ?? 33 - end; + end = start + contentLength - 1; + var httpResponse = actionContext.HttpContext.Response; + Assert.Equal(Path.Combine("TestFiles", "FilePathResultTestFile.txt"), sendFile.Name); + Assert.Equal(start, sendFile.Offset); + Assert.Equal(contentLength, sendFile.Length); + Assert.Equal(CancellationToken.None, sendFile.Token); + var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33); + Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); + Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); + Assert.NotEmpty(httpResponse.Headers.LastModified); + Assert.Equal(contentLength, httpResponse.ContentLength); + } + + public static async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding( + Func function) + { + // Arrange + var expectedContentType = "text/foo; charset=us-ascii"; + var result = new TestVirtualFileResult( + "FilePathResultTestFile_ASCII.txt", expectedContentType) + { + FileProvider = GetFileProvider("FilePathResultTestFile_ASCII.txt"), + }; + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + Assert.Equal(expectedContentType, httpContext.Response.ContentType); + Assert.Equal("FilePathResultTestFile_ASCII.txt", sendFileFeature.Name); + } + + public static async Task ExecuteResultAsync_ReturnsFileContentsForRelativePaths( + Func function) + { + // Arrange + var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); + var result = new TestVirtualFileResult(path, "text/plain") + { + FileProvider = GetFileProvider(path), + }; + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + Assert.Equal(path, sendFileFeature.Name); + } + + public static async Task ExecuteResultAsync_ReturnsFiles_ForDifferentPaths( + string path, + Func function) + { + // Arrange + var result = new TestVirtualFileResult(path, "text/plain") + { + FileProvider = GetFileProvider(path), + }; + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + Mock.Get(result.FileProvider).Verify(); + Assert.Equal(path, sendFileFeature.Name); + } + + public static async Task ExecuteResultAsync_TrimsTilde_BeforeInvokingFileProvider( + string path, + Func function) + { + // Arrange + var expectedPath = path.Substring(1); + var result = new TestVirtualFileResult(path, "text/plain") + { + FileProvider = GetFileProvider(expectedPath), + }; + + var sendFileFeature = new TestSendFileFeature(); + var httpContext = GetHttpContext(); + httpContext.Features.Set(sendFileFeature); + + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(result, (TContext)context); + + // Assert + Mock.Get(result.FileProvider).Verify(); + Assert.Equal(expectedPath, sendFileFeature.Name); + } + + public static async Task ExecuteResultAsync_WorksWithNonDiskBasedFiles( + Func function) + { + // Arrange + var httpContext = GetHttpContext(typeof(VirtualFileResultExecutor)); + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + var expectedData = "This is an embedded resource"; + var sourceStream = new MemoryStream(Encoding.UTF8.GetBytes(expectedData)); + + var nonDiskFileInfo = new Mock(); + nonDiskFileInfo.SetupGet(fi => fi.Exists).Returns(true); + nonDiskFileInfo.SetupGet(fi => fi.PhysicalPath).Returns(() => null); // set null to indicate non-disk file + nonDiskFileInfo.Setup(fi => fi.CreateReadStream()).Returns(sourceStream); + var nonDiskFileProvider = new Mock(); + nonDiskFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())).Returns(nonDiskFileInfo.Object); + + var filePathResult = new VirtualFileResult("/SampleEmbeddedFile.txt", "text/plain") + { + FileProvider = nonDiskFileProvider.Object + }; + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? httpContext : actionContext; + await function(filePathResult, (TContext)context); + + // Assert + httpContext.Response.Body.Position = 0; + var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync(); + Assert.Equal(expectedData, contents); + } + + public static async Task ExecuteResultAsync_ThrowsFileNotFound_IfFileProviderCanNotFindTheFile( + Func function) + { + // Arrange + var path = "TestPath.txt"; + var fileInfo = new Mock(); + fileInfo.SetupGet(f => f.Exists).Returns(false); + var fileProvider = new Mock(); + fileProvider.Setup(f => f.GetFileInfo(path)).Returns(fileInfo.Object); + var filePathResult = new TestVirtualFileResult(path, "text/plain") + { + FileProvider = fileProvider.Object, + }; + + var expectedMessage = "Could not find file: " + path; + var actionContext = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor()); + + // Act + object context = typeof(TContext) == typeof(HttpContext) ? actionContext.HttpContext : actionContext; + var ex = await Assert.ThrowsAsync(() => function(filePathResult, (TContext)context)); + + // Assert + Assert.Equal(expectedMessage, ex.Message); + Assert.Equal(path, ex.FileName); + } + + private static IServiceCollection CreateServices(Type executorType) + { + var services = new ServiceCollection(); + + var hostingEnvironment = new Mock(); + + services.AddSingleton, TestVirtualFileResultExecutor>(); + if (executorType != null) + { + services.AddSingleton(typeof(IActionResultExecutor), executorType); + } + + services.AddSingleton(hostingEnvironment.Object); + services.AddSingleton(NullLoggerFactory.Instance); + + return services; + } + + private static HttpContext GetHttpContext(Type executorType = null) + { + var services = CreateServices(executorType); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services.BuildServiceProvider(); + + return httpContext; + } + + private static IFileProvider GetFileProvider(string path) + { + var fileInfo = new Mock(); + fileInfo.SetupGet(fi => fi.Length).Returns(33); + fileInfo.SetupGet(fi => fi.Exists).Returns(true); + var lastModified = DateTimeOffset.MinValue.AddDays(1); + lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0)); + fileInfo.SetupGet(fi => fi.LastModified).Returns(lastModified); + fileInfo.SetupGet(fi => fi.PhysicalPath).Returns(path); + var fileProvider = new Mock(); + fileProvider.Setup(fp => fp.GetFileInfo(path)) + .Returns(fileInfo.Object) + .Verifiable(); + + return fileProvider.Object; + } + + private class TestVirtualFileResult : VirtualFileResult + { + public TestVirtualFileResult(string filePath, string contentType) + : base(filePath, contentType) + { + } + + public override Task ExecuteResultAsync(ActionContext context) + { + var executor = (TestVirtualFileResultExecutor)context.HttpContext.RequestServices.GetRequiredService>(); + return executor.ExecuteAsync(context, this); + } + } + + private class TestVirtualFileResultExecutor : VirtualFileResultExecutor + { + public TestVirtualFileResultExecutor(ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment) + : base(loggerFactory, hostingEnvironment) + { + } + } + + private class TestSendFileFeature : IHttpResponseBodyFeature + { + public string Name { get; set; } + public long Offset { get; set; } + public long? Length { get; set; } + public CancellationToken Token { get; set; } + + public Stream Stream => throw new NotImplementedException(); + + public PipeWriter Writer => throw new NotImplementedException(); + + public Task CompleteAsync() + { + throw new NotImplementedException(); + } + + public void DisableBuffering() + { + throw new NotImplementedException(); + } + + public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + Name = path; + Offset = offset; + Length = length; + Token = cancellation; + + return Task.FromResult(0); + } + + public Task StartAsync(CancellationToken cancellation = default) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Mvc/Mvc.Core/test/FileContentActionResultTest.cs b/src/Mvc/Mvc.Core/test/FileContentActionResultTest.cs new file mode 100644 index 000000000000..a8e91fddd761 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/FileContentActionResultTest.cs @@ -0,0 +1,155 @@ +// 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.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class FileContentActionResultTest + { + [Fact] + public void Constructor_SetsFileContents() + { + // Arrange + var fileContents = new byte[0]; + + // Act + var result = new FileContentResult(fileContents, "text/plain"); + + // Assert + Assert.Same(fileContents, result.FileContents); + } + + [Fact] + public void Constructor_SetsContentTypeAndParameters() + { + // Arrange + var fileContents = new byte[0]; + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var expectedMediaType = contentType; + + // Act + var result = new FileContentResult(fileContents, contentType); + + // Assert + Assert.Same(fileContents, result.FileContents); + MediaTypeAssert.Equal(expectedMediaType, result.ContentType); + } + + [Fact] + public void Constructor_SetsLastModifiedAndEtag() + { + // Arrange + var fileContents = new byte[0]; + var contentType = "text/plain"; + var expectedMediaType = contentType; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + + // Act + var result = new FileContentResult(fileContents, contentType) + { + LastModified = lastModified, + EntityTag = entityTag + }; + + // Assert + Assert.Equal(lastModified, result.LastModified); + Assert.Equal(entityTag, result.EntityTag); + MediaTypeAssert.Equal(expectedMediaType, result.ContentType); + } + + [Fact] + public async Task WriteFileAsync_CopiesBuffer_ToOutputStream() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_CopiesBuffer_ToOutputStream(action); + } + + [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 BaseFileContentResultTest + .WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(start, end, expectedString, contentLength, action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest(action); + } + + [Fact] + public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored(action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored(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 BaseFileContentResultTest.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 BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(rangeString, action); + } + + [Fact] + public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_PreconditionFailed_RangeRequestedIgnored(action); + } + + [Fact] + public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_NotModified_RangeRequestedIgnored(action); + } + + [Fact] + public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseFileContentResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/FileContentResult.cs b/src/Mvc/Mvc.Core/test/FileContentResult.cs new file mode 100644 index 000000000000..7be596c7ee6a --- /dev/null +++ b/src/Mvc/Mvc.Core/test/FileContentResult.cs @@ -0,0 +1,101 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class FileContentResultTest + { + [Fact] + public async Task WriteFileAsync_CopiesBuffer_ToOutputStream() + {var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + await BaseFileContentResultTest.WriteFileAsync_CopiesBuffer_ToOutputStream(action); + } + + [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 ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest + .WriteFileAsync_PreconditionStateShouldProcess_WritesRangeRequested(start, end, expectedString, contentLength, action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRangeRequest(action); + } + + [Fact] + public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestIgnored(action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestIgnored(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 ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest.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 ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(rangeString, action); + } + + [Fact] + public async Task WriteFileAsync_PreconditionFailed_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_PreconditionFailed_RangeRequestedIgnored(action); + } + + [Fact] + public async Task WriteFileAsync_NotModified_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest.WriteFileAsync_NotModified_RangeRequestedIgnored(action); + } + + [Fact] + public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseFileContentResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/FileResultTest.cs b/src/Mvc/Mvc.Core/test/FileResultTest.cs index 07967adc1eda..25e6d6f3634e 100644 --- a/src/Mvc/Mvc.Core/test/FileResultTest.cs +++ b/src/Mvc/Mvc.Core/test/FileResultTest.cs @@ -268,13 +268,13 @@ public void GetPreconditionState_ShouldProcess(string ifMatch, string ifNoneMatc httpRequestHeaders.IfUnmodifiedSince = lastModified; httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1); actionContext.HttpContext = httpContext; - var fileResult = (new Mock(NullLogger.Instance)).Object; // Act - var state = fileResult.GetPreconditionState( + var state = FileResultExecutorBase.GetPreconditionState( httpRequestHeaders, lastModified, - etag); + etag, + NullLogger.Instance); // Assert Assert.Equal(FileResultExecutorBase.PreconditionState.ShouldProcess, state); @@ -307,13 +307,13 @@ public void GetPreconditionState_ShouldNotProcess_PreconditionFailed(string ifMa httpRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(2); actionContext.HttpContext = httpContext; - var fileResult = (new Mock(NullLogger.Instance)).Object; // Act - var state = fileResult.GetPreconditionState( + var state = FileResultExecutorBase.GetPreconditionState( httpRequestHeaders, lastModified, - etag); + etag, + NullLogger.Instance); // Assert Assert.Equal(FileResultExecutorBase.PreconditionState.PreconditionFailed, state); @@ -344,13 +344,13 @@ public void GetPreconditionState_ShouldNotProcess_NotModified(string ifMatch, st }; httpRequestHeaders.IfModifiedSince = lastModified; actionContext.HttpContext = httpContext; - var fileResult = (new Mock(NullLogger.Instance)).Object; // Act - var state = fileResult.GetPreconditionState( + var state = FileResultExecutorBase.GetPreconditionState( httpRequestHeaders, lastModified, - etag); + etag, + NullLogger.Instance); // Assert Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state); @@ -372,13 +372,13 @@ public void IfRangeValid_IgnoreRangeRequest(string ifRangeString, bool expected) httpRequestHeaders.IfRange = new RangeConditionHeaderValue(ifRangeString); httpRequestHeaders.IfModifiedSince = lastModified; actionContext.HttpContext = httpContext; - var fileResult = (new Mock(NullLogger.Instance)).Object; // Act - var ifRangeIsValid = fileResult.IfRangeValid( + var ifRangeIsValid = FileResultExecutorBase.IfRangeValid( httpRequestHeaders, lastModified, - etag); + etag, + NullLogger.Instance); // Assert Assert.Equal(expected, ifRangeIsValid); diff --git a/src/Mvc/Mvc.Core/test/PhysicalFileActionResultTest.cs b/src/Mvc/Mvc.Core/test/PhysicalFileActionResultTest.cs new file mode 100644 index 000000000000..7677af32f434 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/PhysicalFileActionResultTest.cs @@ -0,0 +1,204 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class PhysicalFileActionResultTest + { + [Fact] + public void Constructor_SetsFileName() + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + + // Act + var result = new PhysicalFileResult(path, "text/plain"); + + // Assert + Assert.Equal(path, result.FileName); + } + + [Fact] + public void Constructor_SetsContentTypeAndParameters() + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var expectedMediaType = contentType; + + // Act + var result = new PhysicalFileResult(path, contentType); + + // Assert + Assert.Equal(path, result.FileName); + MediaTypeAssert.Equal(expectedMediaType, result.ContentType); + } + + [Theory] + [InlineData(0, 3, "File", 4)] + [InlineData(8, 13, "Result", 6)] + [InlineData(null, 5, "ts�", 5)] + [InlineData(8, null, "ResultTestFile contents�", 26)] + public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_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 BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action); + } + + [Fact] + public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action); + } + + [Theory] + [InlineData("0-5")] + [InlineData("bytes = ")] + [InlineData("bytes = 1-4, 5-11")] + public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(rangeString, action); + } + + [Theory] + [InlineData("bytes = 35-36")] + [InlineData("bytes = -0")] + public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeRequestedNotSatisfiable(rangeString, action); + } + + [Fact] + public async Task WriteFileAsync_RangeRequested_PreconditionFailed() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action); + } + + [Fact] + public async Task WriteFileAsync_RangeRequested_NotModified() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_NotModified(action); + } + + [Fact] + public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(action); + } + + [Theory] + [InlineData(0, 3, 4)] + [InlineData(8, 13, 6)] + [InlineData(null, 3, 3)] + [InlineData(8, null, 26)] + public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, long contentLength) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(start, end, contentLength, action); + } + + [Fact] + public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action); + } + + [Fact] + public async Task ExecuteResultAsync_WorksWithAbsolutePaths() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.ExecuteResultAsync_WorksWithAbsolutePaths(action); + } + + [Theory] + [InlineData("FilePathResultTestFile.txt")] + [InlineData("./FilePathResultTestFile.txt")] + [InlineData(".\\FilePathResultTestFile.txt")] + [InlineData("~/FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles/FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")] + [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")] + [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")] + [InlineData("~/SubFolder/SubFolderTestFile.txt")] + [InlineData("~/SubFolder\\SubFolderTestFile.txt")] + public async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(string path) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BasePhysicalFileResultTest.ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(path, action); + } + + [Theory] + [InlineData("/SubFolder/SubFolderTestFile.txt")] + [InlineData("\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("/SubFolder\\SubFolderTestFile.txt")] + [InlineData("\\SubFolder/SubFolderTestFile.txt")] + [InlineData("./SubFolder/SubFolderTestFile.txt")] + [InlineData(".\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("./SubFolder\\SubFolderTestFile.txt")] + [InlineData(".\\SubFolder/SubFolderTestFile.txt")] + public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + BasePhysicalFileResultTest + .ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(path, action); + } + + [Theory] + [InlineData("/FilePathResultTestFile.txt")] + [InlineData("\\FilePathResultTestFile.txt")] + public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + BasePhysicalFileResultTest + .ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(path, action); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/PhysicalFileResult.cs b/src/Mvc/Mvc.Core/test/PhysicalFileResult.cs new file mode 100644 index 000000000000..5cadbb3db739 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/PhysicalFileResult.cs @@ -0,0 +1,173 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class PhysicalFileResultTest + { + [Theory] + [InlineData(0, 3, "File", 4)] + [InlineData(8, 13, "Result", 6)] + [InlineData(null, 5, "ts�", 5)] + [InlineData(8, null, "ResultTestFile contents�", 26)] + public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength) + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_WritesRangeRequested( + start, + end, + expectedString, + contentLength, + action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action); + } + + [Fact] + public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action); + } + + [Theory] + [InlineData("0-5")] + [InlineData("bytes = ")] + [InlineData("bytes = 1-4, 5-11")] + public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString) + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(rangeString, action); + } + + [Theory] + [InlineData("bytes = 35-36")] + [InlineData("bytes = -0")] + public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString) + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeRequestedNotSatisfiable(rangeString, action); + } + + [Fact] + public async Task WriteFileAsync_RangeRequested_PreconditionFailed() + {var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action); + } + + [Fact] + public async Task WriteFileAsync_RangeRequested_NotModified() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.WriteFileAsync_RangeRequested_NotModified(action); + } + + [Fact] + public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(action); + } + + [Theory] + [InlineData(0, 3, 4)] + [InlineData(8, 13, 6)] + [InlineData(null, 3, 3)] + [InlineData(8, null, 26)] + public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, long contentLength) + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(start, end, contentLength, action); + } + + [Fact] + public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action); + } + + [Fact] + public async Task ExecuteResultAsync_WorksWithAbsolutePaths() + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.ExecuteResultAsync_WorksWithAbsolutePaths(action); + } + + [Theory] + [InlineData("FilePathResultTestFile.txt")] + [InlineData("./FilePathResultTestFile.txt")] + [InlineData(".\\FilePathResultTestFile.txt")] + [InlineData("~/FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles/FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")] + [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")] + [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")] + [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")] + [InlineData("~/SubFolder/SubFolderTestFile.txt")] + [InlineData("~/SubFolder\\SubFolderTestFile.txt")] + public async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(string path) + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BasePhysicalFileResultTest.ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(path, action); + } + + [Theory] + [InlineData("/SubFolder/SubFolderTestFile.txt")] + [InlineData("\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("/SubFolder\\SubFolderTestFile.txt")] + [InlineData("\\SubFolder/SubFolderTestFile.txt")] + [InlineData("./SubFolder/SubFolderTestFile.txt")] + [InlineData(".\\SubFolder\\SubFolderTestFile.txt")] + [InlineData("./SubFolder\\SubFolderTestFile.txt")] + [InlineData(".\\SubFolder/SubFolderTestFile.txt")] + public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path) + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + BasePhysicalFileResultTest + .ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(path, action); + } + + [Theory] + [InlineData("/FilePathResultTestFile.txt")] + [InlineData("\\FilePathResultTestFile.txt")] + public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path) + { + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + BasePhysicalFileResultTest + .ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(path, action); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/RedirectActionResultTest.cs b/src/Mvc/Mvc.Core/test/RedirectActionResultTest.cs new file mode 100644 index 000000000000..9b139431fd88 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/RedirectActionResultTest.cs @@ -0,0 +1,96 @@ +// 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 RedirectActionResultTest + { + [Fact] + public void RedirectResult_Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new RedirectResult(url); + + // Assert + Assert.False(result.PreserveMethod); + Assert.False(result.Permanent); + Assert.Same(url, result.Url); + } + + [Fact] + public void RedirectResult_Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new RedirectResult(url, permanent: true); + + // Assert + Assert.False(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.Same(url, result.Url); + } + + [Fact] + public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMethod_SetsResultUrlPermanentAndPreservesMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new RedirectResult(url, permanent: true, preserveMethod: true); + + // Assert + Assert.True(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.Same(url, result.Url); + } + + [Theory] + [InlineData("", "/Home/About", "/Home/About")] + [InlineData("/myapproot", "/test", "/test")] + public async Task Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde( + string appRoot, + string contentPath, + string expectedPath) + { + var action + = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseRedirectResultTest.Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde( + appRoot, + contentPath, + expectedPath, + action); + } + + [Theory] + [InlineData(null, "~/Home/About", "/Home/About")] + [InlineData("/", "~/Home/About", "/Home/About")] + [InlineData("/", "~/", "/")] + [InlineData("", "~/Home/About", "/Home/About")] + [InlineData("/myapproot", "~/", "/myapproot/")] + public async Task Execute_ReturnsAppRelativePath_WhenItStartsWithTilde( + string appRoot, + string contentPath, + string expectedPath) + { + var action = + new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseRedirectResultTest.Execute_ReturnsAppRelativePath_WhenItStartsWithTilde( + appRoot, + contentPath, + expectedPath, + action); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/RedirectResultTest.cs b/src/Mvc/Mvc.Core/test/RedirectResultTest.cs index 481245fee89d..68e310a82aac 100644 --- a/src/Mvc/Mvc.Core/test/RedirectResultTest.cs +++ b/src/Mvc/Mvc.Core/test/RedirectResultTest.cs @@ -4,66 +4,12 @@ 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 RedirectResultTest { - [Fact] - public void RedirectResult_Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new RedirectResult(url); - - // Assert - Assert.False(result.PreserveMethod); - Assert.False(result.Permanent); - Assert.Same(url, result.Url); - } - - [Fact] - public void RedirectResult_Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new RedirectResult(url, permanent: true); - - // Assert - Assert.False(result.PreserveMethod); - Assert.True(result.Permanent); - Assert.Same(url, result.Url); - } - - [Fact] - public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMethod_SetsResultUrlPermanentAndPreservesMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new RedirectResult(url, permanent: true, preserveMethod: true); - - // Assert - Assert.True(result.PreserveMethod); - Assert.True(result.Permanent); - Assert.Same(url, result.Url); - } - [Theory] [InlineData("", "/Home/About", "/Home/About")] [InlineData("/myapproot", "/test", "/test")] @@ -72,18 +18,14 @@ public async Task Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde( string contentPath, string expectedPath) { - // Arrange - var httpContext = GetHttpContext(appRoot); - var actionContext = GetActionContext(httpContext); - var result = new RedirectResult(contentPath); - - // Act - await result.ExecuteResultAsync(actionContext); + var action + = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Assert - // Verifying if Redirect was called with the specific Url and parameter flag. - Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString()); - Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + await BaseRedirectResultTest.Execute_ReturnsContentPath_WhenItDoesNotStartWithTilde( + appRoot, + contentPath, + expectedPath, + action); } [Theory] @@ -97,46 +39,14 @@ public async Task Execute_ReturnsAppRelativePath_WhenItStartsWithTilde( string contentPath, string expectedPath) { - // Arrange - var httpContext = GetHttpContext(appRoot); - var actionContext = GetActionContext(httpContext); - var result = new RedirectResult(contentPath); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - // Verifying if Redirect was called with the specific Url and parameter flag. - Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString()); - Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); - } - - private static ActionContext GetActionContext(HttpContext httpContext) - { - var routeData = new RouteData(); - routeData.Routers.Add(new Mock().Object); + var action + = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - return new ActionContext(httpContext, - routeData, - new ActionDescriptor()); - } - - private static IServiceProvider GetServiceProvider() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton, RedirectResultExecutor>(); - 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 BaseRedirectResultTest.Execute_ReturnsAppRelativePath_WhenItStartsWithTilde( + appRoot, + contentPath, + expectedPath, + action); } } -} \ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/test/VirtualFileActionResultTest .cs b/src/Mvc/Mvc.Core/test/VirtualFileActionResultTest .cs new file mode 100644 index 000000000000..ebc4f89f903e --- /dev/null +++ b/src/Mvc/Mvc.Core/test/VirtualFileActionResultTest .cs @@ -0,0 +1,213 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class VirtualFileActionResultTest + { + [Fact] + public void Constructor_SetsFileName() + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + + // Act + var result = new VirtualFileResult(path, "text/plain"); + + // Assert + Assert.Equal(path, result.FileName); + } + + [Fact] + public void Constructor_SetsContentTypeAndParameters() + { + // Arrange + var path = Path.GetFullPath("helllo.txt"); + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var expectedMediaType = contentType; + + // Act + var result = new VirtualFileResult(path, contentType); + + // Assert + Assert.Equal(path, result.FileName); + MediaTypeAssert.Equal(expectedMediaType, result.ContentType); + } + + [Theory] + [InlineData(0, 3, "File", 4)] + [InlineData(8, 13, "Result", 6)] + [InlineData(null, 4, "ts¡", 4)] + [InlineData(8, null, "ResultTestFile contents¡", 25)] + public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_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 BaseVirtualFileResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action); + } + + [Fact] + public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest + .WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + await BaseVirtualFileResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action); + } + + [Theory] + [InlineData("0-5")] + [InlineData("bytes = ")] + [InlineData("bytes = 1-4, 5-11")] + public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + await BaseVirtualFileResultTest.WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(rangeString, action); + } + + [Theory] + [InlineData("bytes = 35-36")] + [InlineData("bytes = -0")] + public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + await BaseVirtualFileResultTest.WriteFileAsync_RangeRequestedNotSatisfiable(rangeString, action); + } + + [Fact] + public async Task WriteFileAsync_RangeRequested_PreconditionFailed() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + await BaseVirtualFileResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action); + } + + [Fact] + public async Task WriteFileAsync_RangeRequested_NotModified() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_RangeRequested_NotModified(action); + } + + [Fact] + public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest + .ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent(action); + } + + [Fact] + public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest + .ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(action); + } + + [Theory] + [InlineData(0, 3, "File", 4)] + [InlineData(8, 13, "Result", 6)] + [InlineData(null, 3, "ts¡", 3)] + [InlineData(8, null, "ResultTestFile contents¡", 25)] + public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, string expectedString, long contentLength) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent( + start, + end, + expectedString, + contentLength, + action); + } + + [Fact] + public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action); + } + + [Fact] + public async Task ExecuteResultAsync_ReturnsFileContentsForRelativePaths() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_ReturnsFileContentsForRelativePaths(action); + } + + [Theory] + [InlineData("FilePathResultTestFile.txt")] + [InlineData("TestFiles/FilePathResultTestFile.txt")] + [InlineData("TestFiles/../FilePathResultTestFile.txt")] + [InlineData("TestFiles\\FilePathResultTestFile.txt")] + [InlineData("TestFiles\\..\\FilePathResultTestFile.txt")] + [InlineData(@"\\..//?><|""&@#\c:\..\? /..txt")] + public async Task ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(string path) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest + .ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(path, action); + } + + [Theory] + [InlineData("~/FilePathResultTestFile.txt")] + [InlineData("~/TestFiles/FilePathResultTestFile.txt")] + [InlineData("~/TestFiles/../FilePathResultTestFile.txt")] + [InlineData("~/TestFiles\\..\\FilePathResultTestFile.txt")] + [InlineData(@"~~~~\\..//?>~<|""&@#\c:\..\? /..txt~~~")] + public async Task ExecuteResultAsync_TrimsTilde_BeforeInvokingFileProvider(string path) + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest + .ExecuteResultAsync_TrimsTilde_BeforeInvokingFileProvider(path, action); + } + + [Fact] + public async Task ExecuteResultAsync_WorksWithNonDiskBasedFiles() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_WorksWithNonDiskBasedFiles(action); + } + + [Fact] + public async Task ExecuteResultAsync_ThrowsFileNotFound_IfFileProviderCanNotFindTheFile() + { + var action = new Func(async (result, context) => await result.ExecuteResultAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_ThrowsFileNotFound_IfFileProviderCanNotFindTheFile(action); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/VirtualFileResultTest.cs b/src/Mvc/Mvc.Core/test/VirtualFileResultTest.cs index 6d4ecfe35150..04a1f14037d4 100644 --- a/src/Mvc/Mvc.Core/test/VirtualFileResultTest.cs +++ b/src/Mvc/Mvc.Core/test/VirtualFileResultTest.cs @@ -2,58 +2,14 @@ // 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.IO.Pipelines; -using System.Text; -using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Net.Http.Headers; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc { public class VirtualFileResultTest { - [Fact] - public void Constructor_SetsFileName() - { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - - // Act - var result = new TestVirtualFileResult(path, "text/plain"); - - // Assert - Assert.Equal(path, result.FileName); - } - - [Fact] - public void Constructor_SetsContentTypeAndParameters() - { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var expectedMediaType = contentType; - - // Act - var result = new TestVirtualFileResult(path, contentType); - - // Assert - Assert.Equal(path, result.FileName); - MediaTypeAssert.Equal(expectedMediaType, result.ContentType); - } - [Theory] [InlineData(0, 3, "File", 4)] [InlineData(8, 13, "Result", 6)] @@ -61,172 +17,38 @@ public void Constructor_SetsContentTypeAndParameters() [InlineData(8, null, "ResultTestFile contents¡", 25)] public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, string expectedString, long contentLength) { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var result = new TestVirtualFileResult(path, contentType); - result.EnableRangeProcessing = true; - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.Range = new RangeHeaderValue(start, end); - requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1); - httpContext.Request.Method = HttpMethods.Get; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var startResult = start ?? 33 - end; - var endResult = startResult + contentLength - 1; - var httpResponse = actionContext.HttpContext.Response; - var contentRange = new ContentRangeHeaderValue(startResult.Value, endResult.Value, 33); - Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); - Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); - Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); - Assert.NotEmpty(httpResponse.Headers.LastModified); - Assert.Equal(contentLength, httpResponse.ContentLength); - Assert.Equal(path, sendFileFeature.Name); - Assert.Equal(startResult, sendFileFeature.Offset); - Assert.Equal((long?)contentLength, sendFileFeature.Length); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_WritesRangeRequested( + start, + end, + expectedString, + contentLength, + action); } [Fact] public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var result = new TestVirtualFileResult(path, contentType); - result.EnableRangeProcessing = true; - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; - requestHeaders.Range = new RangeHeaderValue(0, 3); - requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); - httpContext.Request.Method = HttpMethods.Get; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); - Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); - var contentRange = new ContentRangeHeaderValue(0, 3, 33); - Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - Assert.Equal(4, httpResponse.ContentLength); - Assert.Equal(path, sendFileFeature.Name); - Assert.Equal(0, sendFileFeature.Offset); - Assert.Equal(4, sendFileFeature.Length); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange(action); } [Fact] public async Task WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored() { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var result = new TestVirtualFileResult(path, contentType); - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; - requestHeaders.Range = new RangeHeaderValue(0, 3); - requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); - httpContext.Request.Method = HttpMethods.Get; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - Assert.Equal(path, sendFileFeature.Name); - Assert.Equal(0, sendFileFeature.Offset); - Assert.Null(sendFileFeature.Length); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_RangeProcessingNotEnabled_RangeRequestedIgnored(action); } [Fact] public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var result = new TestVirtualFileResult(path, contentType); - result.EnableRangeProcessing = true; - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; - requestHeaders.Range = new RangeHeaderValue(0, 3); - requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\"")); - httpContext.Request.Method = HttpMethods.Get; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); - Assert.Equal(entityTag.ToString(), httpResponse.Headers.ETag); - Assert.Equal(path, sendFileFeature.Name); - Assert.Equal(0, sendFileFeature.Offset); - Assert.Null(sendFileFeature.Length); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored(action); } [Theory] @@ -235,41 +57,9 @@ public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedIgnored() [InlineData("bytes = 1-4, 5-11")] public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string rangeString) { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var result = new TestVirtualFileResult(path, contentType); - result.EnableRangeProcessing = true; - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var requestHeaders = httpContext.Request.GetTypedHeaders(); - httpContext.Request.Headers.Range = rangeString; - requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1); - httpContext.Request.Method = HttpMethods.Get; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); - Assert.Empty(httpResponse.Headers.ContentRange); - Assert.NotEmpty(httpResponse.Headers.LastModified); - Assert.Equal(path, sendFileFeature.Name); - Assert.Equal(0, sendFileFeature.Offset); - Assert.Null(sendFileFeature.Length); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(rangeString, action); } [Theory] @@ -277,180 +67,43 @@ public async Task WriteFileAsync_RangeHeaderMalformed_RangeRequestIgnored(string [InlineData("bytes = -0")] public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString) { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var result = new TestVirtualFileResult(path, contentType); - result.EnableRangeProcessing = true; - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var httpContext = GetHttpContext(); - httpContext.Response.Body = new MemoryStream(); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var requestHeaders = httpContext.Request.GetTypedHeaders(); - httpContext.Request.Headers.Range = rangeString; - requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1); - 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; - var contentRange = new ContentRangeHeaderValue(33); - Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode); - Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); - Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); - Assert.NotEmpty(httpResponse.Headers.LastModified); - Assert.Equal(0, httpResponse.ContentLength); - Assert.Empty(body); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_RangeRequestedNotSatisfiable(rangeString, action); } [Fact] public async Task WriteFileAsync_RangeRequested_PreconditionFailed() { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var result = new TestVirtualFileResult(path, contentType); - result.EnableRangeProcessing = true; - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; - httpContext.Request.Headers.Range = "bytes = 0-6"; - httpContext.Request.Method = HttpMethods.Get; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode); - Assert.Null(httpResponse.ContentLength); - Assert.Empty(httpResponse.Headers.ContentRange); - Assert.NotEmpty(httpResponse.Headers.LastModified); - Assert.Null(sendFileFeature.Name); // Not called + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_RangeRequested_PreconditionFailed(action); } [Fact] public async Task WriteFileAsync_RangeRequested_NotModified() { - // Arrange - var path = Path.GetFullPath("helllo.txt"); - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var result = new TestVirtualFileResult(path, contentType); - result.EnableRangeProcessing = true; - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1); - httpContext.Request.Headers.Range = "bytes = 0-6"; - httpContext.Request.Method = HttpMethods.Get; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - var httpResponse = actionContext.HttpContext.Response; - Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode); - Assert.Null(httpResponse.ContentLength); - Assert.Empty(httpResponse.Headers.ContentRange); - Assert.NotEmpty(httpResponse.Headers.LastModified); - Assert.False(httpResponse.Headers.ContainsKey(HeaderNames.ContentType)); - Assert.Null(sendFileFeature.Name); // Not called + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.WriteFileAsync_RangeRequested_NotModified(action); } [Fact] public async Task ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent() { - // Arrange - var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); - var result = new TestVirtualFileResult(path, "text/plain"); - - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(context); - - // Assert - Assert.Equal(path, sendFileFeature.Name); - Assert.Equal(0, sendFileFeature.Offset); - Assert.Null(sendFileFeature.Length); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest + .ExecuteResultAsync_FallsBackToWebRootFileProvider_IfNoFileProviderIsPresent(action); } [Fact] public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() { - // Arrange - var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); - var result = new TestVirtualFileResult(path, "text/plain") - { - FileProvider = GetFileProvider(path), - }; - - var sendFileMock = new Mock(); - sendFileMock - .Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None)) - .Returns(Task.FromResult(0)); - - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileMock.Object); - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(context); - - // Assert - sendFileMock.Verify(); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest + .ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent(action); } [Theory] @@ -460,96 +113,30 @@ public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() [InlineData(8, null, "ResultTestFile contents¡", 25)] public async Task ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent(long? start, long? end, string expectedString, long contentLength) { - // Arrange - var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); - var result = new TestVirtualFileResult(path, "text/plain") - { - FileProvider = GetFileProvider(path), - EnableRangeProcessing = true, - }; - - var sendFile = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFile); - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var appEnvironment = new Mock(); - appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient, TestVirtualFileResultExecutor>() - .AddTransient() - .BuildServiceProvider(); - - var requestHeaders = httpContext.Request.GetTypedHeaders(); - requestHeaders.Range = new RangeHeaderValue(start, end); - requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1); - httpContext.Request.Method = HttpMethods.Get; - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(actionContext); - - // Assert - start = start ?? 33 - end; - end = start + contentLength - 1; - var httpResponse = actionContext.HttpContext.Response; - Assert.Equal(Path.Combine("TestFiles", "FilePathResultTestFile.txt"), sendFile.Name); - Assert.Equal(start, sendFile.Offset); - Assert.Equal(contentLength, sendFile.Length); - Assert.Equal(CancellationToken.None, sendFile.Token); - var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33); - Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode); - Assert.Equal("bytes", httpResponse.Headers.AcceptRanges); - Assert.Equal(contentRange.ToString(), httpResponse.Headers.ContentRange); - Assert.NotEmpty(httpResponse.Headers.LastModified); - Assert.Equal(contentLength, httpResponse.ContentLength); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_CallsSendFileAsyncWithRequestedRange_IfIHttpSendFilePresent( + start, + end, + expectedString, + contentLength, + action); } [Fact] public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() { - // Arrange - var expectedContentType = "text/foo; charset=us-ascii"; - var result = new TestVirtualFileResult( - "FilePathResultTestFile_ASCII.txt", expectedContentType) - { - FileProvider = GetFileProvider("FilePathResultTestFile_ASCII.txt"), - }; - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(context); - - // Assert - Assert.Equal(expectedContentType, httpContext.Response.ContentType); - Assert.Equal("FilePathResultTestFile_ASCII.txt", sendFileFeature.Name); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding(action); } [Fact] public async Task ExecuteResultAsync_ReturnsFileContentsForRelativePaths() { - // Arrange - var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt"); - var result = new TestVirtualFileResult(path, "text/plain") - { - FileProvider = GetFileProvider(path), - }; - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(context); - - // Assert - Assert.Equal(path, sendFileFeature.Name); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_ReturnsFileContentsForRelativePaths(action); } [Theory] @@ -561,24 +148,10 @@ public async Task ExecuteResultAsync_ReturnsFileContentsForRelativePaths() [InlineData(@"\\..//?><|""&@#\c:\..\? /..txt")] public async Task ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(string path) { - // Arrange - var result = new TestVirtualFileResult(path, "text/plain") - { - FileProvider = GetFileProvider(path), - }; - - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(context); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - // Assert - Mock.Get(result.FileProvider).Verify(); - Assert.Equal(path, sendFileFeature.Name); + await BaseVirtualFileResultTest + .ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(path, action); } [Theory] @@ -589,185 +162,26 @@ public async Task ExecuteResultAsync_ReturnsFiles_ForDifferentPaths(string path) [InlineData(@"~~~~\\..//?>~<|""&@#\c:\..\? /..txt~~~")] public async Task ExecuteResultAsync_TrimsTilde_BeforeInvokingFileProvider(string path) { - // Arrange - var expectedPath = path.Substring(1); - var result = new TestVirtualFileResult(path, "text/plain") - { - FileProvider = GetFileProvider(expectedPath), - }; + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - var sendFileFeature = new TestSendFileFeature(); - var httpContext = GetHttpContext(); - httpContext.Features.Set(sendFileFeature); - - var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - - // Act - await result.ExecuteResultAsync(context); - - // Assert - Mock.Get(result.FileProvider).Verify(); - Assert.Equal(expectedPath, sendFileFeature.Name); + await BaseVirtualFileResultTest + .ExecuteResultAsync_TrimsTilde_BeforeInvokingFileProvider(path, action); } [Fact] public async Task ExecuteResultAsync_WorksWithNonDiskBasedFiles() { - // Arrange - var httpContext = GetHttpContext(typeof(VirtualFileResultExecutor)); - httpContext.Response.Body = new MemoryStream(); - var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); - var expectedData = "This is an embedded resource"; - var sourceStream = new MemoryStream(Encoding.UTF8.GetBytes(expectedData)); - - var nonDiskFileInfo = new Mock(); - nonDiskFileInfo.SetupGet(fi => fi.Exists).Returns(true); - nonDiskFileInfo.SetupGet(fi => fi.PhysicalPath).Returns(() => null); // set null to indicate non-disk file - nonDiskFileInfo.Setup(fi => fi.CreateReadStream()).Returns(sourceStream); - var nonDiskFileProvider = new Mock(); - nonDiskFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())).Returns(nonDiskFileInfo.Object); - - var filePathResult = new VirtualFileResult("/SampleEmbeddedFile.txt", "text/plain") - { - FileProvider = nonDiskFileProvider.Object - }; - - // Act - await filePathResult.ExecuteResultAsync(actionContext); - - // Assert - httpContext.Response.Body.Position = 0; - var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync(); - Assert.Equal(expectedData, contents); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); + + await BaseVirtualFileResultTest.ExecuteResultAsync_WorksWithNonDiskBasedFiles(action); } [Fact] public async Task ExecuteResultAsync_ThrowsFileNotFound_IfFileProviderCanNotFindTheFile() { - // Arrange - var path = "TestPath.txt"; - var fileInfo = new Mock(); - fileInfo.SetupGet(f => f.Exists).Returns(false); - var fileProvider = new Mock(); - fileProvider.Setup(f => f.GetFileInfo(path)).Returns(fileInfo.Object); - var filePathResult = new TestVirtualFileResult(path, "text/plain") - { - FileProvider = fileProvider.Object, - }; - - var expectedMessage = "Could not find file: " + path; - var context = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor()); - - // Act - var ex = await Assert.ThrowsAsync(() => filePathResult.ExecuteResultAsync(context)); - - // Assert - Assert.Equal(expectedMessage, ex.Message); - Assert.Equal(path, ex.FileName); - } - - private static IServiceCollection CreateServices(Type executorType) - { - var services = new ServiceCollection(); - - var hostingEnvironment = new Mock(); - - services.AddSingleton, TestVirtualFileResultExecutor>(); - if (executorType != null) - { - services.AddSingleton(typeof(IActionResultExecutor), executorType); - } - - services.AddSingleton(hostingEnvironment.Object); - services.AddSingleton(NullLoggerFactory.Instance); - - return services; - } - - private static HttpContext GetHttpContext(Type executorType = null) - { - var services = CreateServices(executorType); + var action = new Func(async (result, context) => await ((IResult)result).ExecuteAsync(context)); - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = services.BuildServiceProvider(); - - return httpContext; - } - - private static IFileProvider GetFileProvider(string path) - { - var fileInfo = new Mock(); - fileInfo.SetupGet(fi => fi.Length).Returns(33); - fileInfo.SetupGet(fi => fi.Exists).Returns(true); - var lastModified = DateTimeOffset.MinValue.AddDays(1); - lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0)); - fileInfo.SetupGet(fi => fi.LastModified).Returns(lastModified); - fileInfo.SetupGet(fi => fi.PhysicalPath).Returns(path); - var fileProvider = new Mock(); - fileProvider.Setup(fp => fp.GetFileInfo(path)) - .Returns(fileInfo.Object) - .Verifiable(); - - return fileProvider.Object; - } - - private class TestVirtualFileResult : VirtualFileResult - { - public TestVirtualFileResult(string filePath, string contentType) - : base(filePath, contentType) - { - } - - public override Task ExecuteResultAsync(ActionContext context) - { - var executor = (TestVirtualFileResultExecutor)context.HttpContext.RequestServices.GetRequiredService>(); - return executor.ExecuteAsync(context, this); - } - } - - private class TestVirtualFileResultExecutor : VirtualFileResultExecutor - { - public TestVirtualFileResultExecutor(ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment) - : base(loggerFactory, hostingEnvironment) - { - } - } - - private class TestSendFileFeature : IHttpResponseBodyFeature - { - public string Name { get; set; } - public long Offset { get; set; } - public long? Length { get; set; } - public CancellationToken Token { get; set; } - - public Stream Stream => throw new NotImplementedException(); - - public PipeWriter Writer => throw new NotImplementedException(); - - public Task CompleteAsync() - { - throw new NotImplementedException(); - } - - public void DisableBuffering() - { - throw new NotImplementedException(); - } - - public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) - { - Name = path; - Offset = offset; - Length = length; - Token = cancellation; - - return Task.FromResult(0); - } - - public Task StartAsync(CancellationToken cancellation = default) - { - throw new NotImplementedException(); - } + await BaseVirtualFileResultTest.ExecuteResultAsync_ThrowsFileNotFound_IfFileProviderCanNotFindTheFile(action); } } -} \ No newline at end of file +}