From 7ffd88757de0228f25be39fc2da76a87e9bdac57 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Tue, 6 Jun 2017 11:22:35 -0700 Subject: [PATCH] Respond to RangeHelper refactor (#6348) Respond to https://github.com/aspnet/StaticFiles/pull/200 --- .../Internal/FileResultExecutorBase.cs | 155 +++++++++--------- .../FileContentResultTest.cs | 41 ++++- .../FileResultTest.cs | 12 +- .../FileStreamResultTest.cs | 44 ++++- .../PhysicalFileResultTest.cs | 37 ++++- .../VirtualFileResultTest.cs | 57 ++++++- .../FileResultTests.cs | 113 ++++++++++++- 7 files changed, 355 insertions(+), 104 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs index 2a64c74601..80f6aabdee 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FileResultExecutorBase.cs @@ -31,8 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal Unspecified, NotModified, ShouldProcess, - PreconditionFailed, - IgnoreRangeRequest + PreconditionFailed } protected ILogger Logger { get; } @@ -61,15 +60,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal var httpRequestHeaders = request.GetTypedHeaders(); var response = context.HttpContext.Response; var httpResponseHeaders = response.GetTypedHeaders(); - if (fileLength.HasValue) - { - SetAcceptRangeHeader(context); - // Assuming the request is not a range request, the Content-Length header is set to the length of the entire file. - // If the request is a valid range request, this header is overwritten with the length of the range as part of the - // range processing (see method SetContentLength). - response.ContentLength = fileLength.Value; - } - if (lastModified.HasValue) { httpResponseHeaders.LastModified = lastModified; @@ -80,24 +70,35 @@ namespace Microsoft.AspNetCore.Mvc.Internal } var serveBody = !HttpMethods.IsHead(request.Method); - if (HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method)) + var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag); + if (preconditionState == PreconditionState.NotModified) { - var preconditionState = GetPreconditionState(context, httpRequestHeaders, lastModified, etag); - if (request.Headers.ContainsKey(HeaderNames.Range) && - (preconditionState == PreconditionState.Unspecified || - preconditionState == PreconditionState.ShouldProcess)) + serveBody = false; + response.StatusCode = StatusCodes.Status304NotModified; + } + else if (preconditionState == PreconditionState.PreconditionFailed) + { + serveBody = false; + response.StatusCode = StatusCodes.Status412PreconditionFailed; + } + + if (fileLength.HasValue) + { + SetAcceptRangeHeader(context); + // Assuming the request is not a range request, the Content-Length header is set to the length of the entire file. + // If the request is a valid range request, this header is overwritten with the length of the range as part of the + // range processing (see method SetContentLength). + response.ContentLength = fileLength.Value; + if (HttpMethods.IsHead(request.Method) || HttpMethods.IsGet(request.Method)) { - return SetRangeHeaders(context, httpRequestHeaders, fileLength, lastModified, etag); - } - if (preconditionState == PreconditionState.NotModified) - { - serveBody = false; - response.StatusCode = StatusCodes.Status304NotModified; - } - else if (preconditionState == PreconditionState.PreconditionFailed) - { - serveBody = false; - response.StatusCode = StatusCodes.Status412PreconditionFailed; + if ((preconditionState == PreconditionState.Unspecified || + preconditionState == PreconditionState.ShouldProcess)) + { + if (IfRangeValid(context, httpRequestHeaders, lastModified, etag)) + { + return SetRangeHeaders(context, httpRequestHeaders, fileLength); + } + } } } @@ -155,6 +156,37 @@ namespace Microsoft.AspNetCore.Mvc.Internal return PreconditionState.Unspecified; } + internal static bool IfRangeValid( + ActionContext context, + RequestHeaders httpRequestHeaders, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue etag = null) + { + // 14.27 If-Range + var ifRange = httpRequestHeaders.IfRange; + if (ifRange != null) + { + // If the validator given in the If-Range header field matches the + // current validator for the selected representation of the target + // resource, then the server SHOULD process the Range header field as + // requested. If the validator does not match, the server MUST ignore + // the Range header field. + if (ifRange.LastModified.HasValue) + { + if (lastModified.HasValue && lastModified > ifRange.LastModified) + { + return false; + } + } + else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true)) + { + return false; + } + } + + return true; + } + // Internal for testing internal static PreconditionState GetPreconditionState( ActionContext context, @@ -166,7 +198,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal var ifNoneMatchState = PreconditionState.Unspecified; var ifModifiedSinceState = PreconditionState.Unspecified; var ifUnmodifiedSinceState = PreconditionState.Unspecified; - var ifRangeState = PreconditionState.Unspecified; // 14.24 If-Match var ifMatch = httpRequestHeaders.IfMatch; @@ -208,28 +239,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed; } - var ifRange = httpRequestHeaders.IfRange; - if (ifRange != null) - { - // If the validator given in the If-Range header field matches the - // current validator for the selected representation of the target - // resource, then the server SHOULD process the Range header field as - // requested. If the validator does not match, the server MUST ignore - // the Range header field. - if (ifRange.LastModified.HasValue) - { - if (lastModified.HasValue && lastModified > ifRange.LastModified) - { - ifRangeState = PreconditionState.IgnoreRangeRequest; - } - } - else if (etag != null && ifRange.EntityTag != null && !ifRange.EntityTag.Compare(etag, useStrongComparison: true)) - { - ifRangeState = PreconditionState.IgnoreRangeRequest; - } - } - - var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState, ifRangeState); + var state = GetMaxPreconditionState(ifMatchState, ifNoneMatchState, ifModifiedSinceState, ifUnmodifiedSinceState); return state; } @@ -250,16 +260,23 @@ namespace Microsoft.AspNetCore.Mvc.Internal private static (RangeItemHeaderValue range, long rangeLength, bool serveBody) SetRangeHeaders( ActionContext context, RequestHeaders httpRequestHeaders, - long? fileLength, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue etag = null) + long? fileLength) { var response = context.HttpContext.Response; var httpResponseHeaders = response.GetTypedHeaders(); - // Checked for presence of Range header explicitly before calling this method. - // Range may be null for parsing errors, multiple ranges and when the file length is missing. - var range = fileLength.HasValue ? ParseRange(context, httpRequestHeaders, fileLength.Value, lastModified, etag) : null; + // 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, + httpRequestHeaders, + fileLength.Value); + + if (!isRangeRequest) + { + return (range: null, rangeLength: 0, serveBody: true); + } + if (range == null) { // 14.16 Content-Range - A server sending a response with status code 416 (Requested range not satisfiable) @@ -295,32 +312,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal return length; } - private static RangeItemHeaderValue ParseRange( - ActionContext context, - RequestHeaders httpRequestHeaders, - long fileLength, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue etag = null) - { - var httpContext = context.HttpContext; - var response = httpContext.Response; - - var range = RangeHelper.ParseRange(httpContext, httpRequestHeaders, lastModified, etag); - - if (range != null) - { - var normalizedRanges = RangeHelper.NormalizeRanges(range, fileLength); - if (normalizedRanges == null || normalizedRanges.Count == 0) - { - return null; - } - - return normalizedRanges.Single(); - } - - return null; - } - protected static ILogger CreateLogger(ILoggerFactory factory) { if (factory == null) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs index cd3b94e62b..64977aaee5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs @@ -233,8 +233,47 @@ namespace Microsoft.AspNetCore.Mvc [Theory] [InlineData("0-5")] - [InlineData("bytes = 11-0")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] + public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString) + { + // Arrange + var contentType = "text/plain"; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + + var result = new FileContentResult(byteArray, contentType) + { + LastModified = lastModified, + EntityTag = entityTag + }; + + var httpContext = GetHttpContext(); + httpContext.Request.Headers[HeaderNames.Range] = rangeString; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Equal("Hello World", body); + } + + [Theory] + [InlineData("bytes = 12-13")] + [InlineData("bytes = -0")] public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString) { // Arrange diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs index aea1c3dab1..55c86fd3cc 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs @@ -378,8 +378,10 @@ namespace Microsoft.AspNetCore.Mvc Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state); } - [Fact] - public void GetPreconditionState_ShouldNotProcess_IgnoreRangeRequest() + [Theory] + [InlineData("\"NotEtag\"", false)] + [InlineData("\"Etag\"", true)] + public void IfRangeValid_IgnoreRangeRequest(string ifRangeString, bool expected) { // Arrange var actionContext = new ActionContext(); @@ -389,19 +391,19 @@ namespace Microsoft.AspNetCore.Mvc var lastModified = DateTimeOffset.MinValue; lastModified = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0)); var etag = new EntityTagHeaderValue("\"Etag\""); - httpRequestHeaders.IfRange = new RangeConditionHeaderValue("\"NotEtag\""); + httpRequestHeaders.IfRange = new RangeConditionHeaderValue(ifRangeString); httpRequestHeaders.IfModifiedSince = lastModified; actionContext.HttpContext = httpContext; // Act - var state = FileResultExecutorBase.GetPreconditionState( + var ifRangeIsValid = FileResultExecutorBase.IfRangeValid( actionContext, httpRequestHeaders, lastModified, etag); // Assert - Assert.Equal(FileResultExecutorBase.PreconditionState.IgnoreRangeRequest, state); + Assert.Equal(expected, ifRangeIsValid); } private static IServiceCollection CreateServices() diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs index 4ff7ecc8ec..e4b342b782 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs @@ -219,9 +219,9 @@ namespace Microsoft.AspNetCore.Mvc [Theory] [InlineData("0-5")] - [InlineData("bytes = 11-0")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] - public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString) + public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestIgnored(string rangeString) { // Arrange var contentType = "text/plain"; @@ -246,6 +246,46 @@ namespace Microsoft.AspNetCore.Mvc // Act await result.ExecuteResultAsync(actionContext); + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Equal("Hello World", body); + } + + [Theory] + [InlineData("bytes = 12-13")] + [InlineData("bytes = -0")] + public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString) + { + // Arrange + var contentType = "text/plain"; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + }; + + var httpContext = GetHttpContext(); + httpContext.Request.Headers[HeaderNames.Range] = rangeString; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + // Assert var httpResponse = actionContext.HttpContext.Response; httpResponse.Body.Seek(0, SeekOrigin.Begin); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs index 0bc08d9746..77e82bf686 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs @@ -117,6 +117,7 @@ namespace Microsoft.AspNetCore.Mvc Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); var contentRange = new ContentRangeHeaderValue(0, 3, 34); Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]); + Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); Assert.Equal(4, httpResponse.ContentLength); Assert.Equal("File", body); @@ -148,14 +149,46 @@ namespace Microsoft.AspNetCore.Mvc var body = streamReader.ReadToEndAsync().Result; Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); Assert.Equal("FilePathResultTestFile contents�", body); } [Theory] [InlineData("0-5")] - [InlineData("bytes = 11-0")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] - public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString) + public async Task WriteFileAsync_RangeRequestIgnored(string rangeString) + { + // Arrange + var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); + var result = new TestPhysicalFileResult(path, "text/plain"); + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfModifiedSince = DateTimeOffset.MinValue; + httpContext.Request.Headers[HeaderNames.Range] = rangeString; + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); + Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal("FilePathResultTestFile contents�", body); + } + + [Theory] + [InlineData("bytes = 35-36")] + [InlineData("bytes = -0")] + public async Task WriteFileAsync_RangeRequestedNotSatisfiable(string rangeString) { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs index 5c424ab4d2..07b5f7e72c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs @@ -191,9 +191,9 @@ namespace Microsoft.AspNetCore.Mvc [Theory] [InlineData("0-5")] - [InlineData("bytes = 11-0")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] - public async Task WriteFileAsync_RangeRequested_NotSatisfiable(string rangeString) + public async Task WriteFileAsync_RangeRequestIgnored(string rangeString) { // Arrange var path = Path.GetFullPath("helllo.txt"); @@ -201,15 +201,58 @@ namespace Microsoft.AspNetCore.Mvc var result = new TestVirtualFileResult(path, contentType); var appEnvironment = new Mock(); appEnvironment.Setup(app => app.WebRootFileProvider) - .Returns(GetFileProvider(path)); + .Returns(GetFileProvider(path)); var httpContext = GetHttpContext(); httpContext.Response.Body = new MemoryStream(); httpContext.RequestServices = new ServiceCollection() - .AddSingleton(appEnvironment.Object) - .AddTransient() - .AddTransient() - .BuildServiceProvider(); + .AddSingleton(appEnvironment.Object) + .AddTransient() + .AddTransient() + .BuildServiceProvider(); + + var requestHeaders = httpContext.Request.GetTypedHeaders(); + httpContext.Request.Headers[HeaderNames.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; + Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]); + Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal("FilePathResultTestFile contents¡", body); + } + + [Theory] + [InlineData("bytes = 35-36")] + [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); + 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() + .AddTransient() + .BuildServiceProvider(); var requestHeaders = httpContext.Request.GetTypedHeaders(); httpContext.Request.Headers[HeaderNames.Range] = rangeString; diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs index a87f20754d..b631f22490 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs @@ -64,8 +64,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests [Theory] [InlineData("0-6")] - [InlineData("bytes = 11-6")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] + public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestIgnored(string rangeString) + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDisk"); + httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("This is a sample text file", body); + } + + [Theory] + [InlineData("bytes = 35-36")] + [InlineData("bytes = -0")] public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable(string rangeString) { // Arrange @@ -107,8 +127,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests [Theory] [InlineData("0-6")] - [InlineData("bytes = 11-6")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] + public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestIgnored_WithLastModifiedAndEtag(string rangeString) + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag"); + httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("This is a sample text file", body); + } + + [Theory] + [InlineData("bytes = 35-36")] + [InlineData("bytes = -0")] public async Task FileFromDisk_CanBeEnabled_WithMiddleware_RangeRequestNotSatisfiable_WithLastModifiedAndEtag(string rangeString) { // Arrange @@ -229,8 +269,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests [Theory] [InlineData("0-6")] - [InlineData("bytes = 11-6")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] + public async Task FileFromStream_ReturnsFile_RangeRequestIgnored(string rangeString) + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStream"); + httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("This is sample text from a stream", body); + } + + [Theory] + [InlineData("bytes = 35-36")] + [InlineData("bytes = -0")] public async Task FileFromStream_ReturnsFile_RangeRequestNotSatisfiable(string rangeString) { // Arrange @@ -349,8 +409,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests [Theory] [InlineData("0-6")] - [InlineData("bytes = 11-6")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] + public async Task FileFromBinaryData_ReturnsFile_RangeRequestIgnored(string rangeString) + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryData"); + httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("This is a sample text from a binary array", body); + } + + [Theory] + [InlineData("bytes = 45-46")] + [InlineData("bytes = -0")] public async Task FileFromBinaryData_ReturnsFile_RangeRequestNotSatisfiable(string rangeString) { // Arrange @@ -524,8 +604,31 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests [Theory] [InlineData("0-6")] - [InlineData("bytes = 11-6")] + [InlineData("bytes = ")] [InlineData("bytes = 1-4, 5-11")] + public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequestIgnored(string rangeString) + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName"); + httpRequestMessage.Headers.TryAddWithoutValidation("Range", rangeString); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal("Sample text file as embedded resource.", body); + var contentDisposition = response.Content.Headers.ContentDisposition.ToString(); + Assert.NotNull(contentDisposition); + Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); + } + + [Theory] + [InlineData("bytes = 45-46")] + [InlineData("bytes = -0")] public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequestNotSatisfiable(string rangeString) { // Arrange