diff --git a/src/Microsoft.AspNetCore.Mvc.Core/SerializableError.cs b/src/Microsoft.AspNetCore.Mvc.Core/SerializableError.cs index 25e0f28c1a..77adaeae1e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/SerializableError.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/SerializableError.cs @@ -26,7 +26,7 @@ public SerializableError() /// /// Creates a new instance of . /// - /// containing the validation errors. + /// containing the validation errors. public SerializableError(ModelStateDictionary modelState) : this() { @@ -57,4 +57,4 @@ public SerializableError(ModelStateDictionary modelState) } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs index 235f581d21..d7e12895a3 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileContentResultTest.cs @@ -144,11 +144,100 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque Assert.Equal(expectedString, body); } + [Fact] + public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() + { + // Arrange + var contentType = "text/plain"; + var lastModified = DateTimeOffset.MinValue; + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + + var result = new FileContentResult(byteArray, contentType) + { + LastModified = lastModified, + EntityTag = entityTag + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + requestHeaders.Range = new RangeHeaderValue(0, 4); + requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue); + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + + // 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.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length); + Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Equal(5, httpResponse.ContentLength); + Assert.Equal("Hello", body); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedNotSatisfiable() + { + // Arrange + var contentType = "text/plain"; + var lastModified = DateTimeOffset.MinValue.AddDays(1); + 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(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + requestHeaders.Range = new RangeHeaderValue(0, 4); + requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\"")); + 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(byteArray.Length); + Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Empty(body); + } + [Theory] [InlineData("0-5")] [InlineData("bytes = 11-0")] [InlineData("bytes = 1-4, 5-11")] - public async Task WriteFileAsync__PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString) + public async Task WriteFileAsync_PreconditionStateUnspecified_RangeRequestedNotSatisfiable(string rangeString) { // Arrange var contentType = "text/plain"; @@ -163,7 +252,6 @@ public async Task WriteFileAsync__PreconditionStateUnspecified_RangeRequestedNot }; var httpContext = GetHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); httpContext.Request.Headers[HeaderNames.Range] = rangeString; httpContext.Request.Method = HttpMethods.Get; httpContext.Response.Body = new MemoryStream(); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs index 0d09067b16..bc06c0fd4d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileResultTest.cs @@ -270,7 +270,7 @@ public async Task SetsAcceptRangeHeader() [InlineData("\"Etag\"", "\"NotEtag\"")] [InlineData("\"Etag\"", null)] [InlineData(null, "\"NotEtag\"")] - public void ComputeConditionalHeaders_ShouldProcess(string ifMatch, string ifNoneMatch) + public void GetPreconditionState_ShouldProcess(string ifMatch, string ifNoneMatch) { var actionContext = new ActionContext(); var httpContext = new DefaultHttpContext(); @@ -293,21 +293,21 @@ public void ComputeConditionalHeaders_ShouldProcess(string ifMatch, string ifNon httpRequestHeaders.IfUnmodifiedSince = lastModified; httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1); actionContext.HttpContext = httpContext; - var shouldProcess = FileResultExecutorBase.GetPreconditionState( + var state = FileResultExecutorBase.GetPreconditionState( actionContext, httpRequestHeaders, lastModified, etag); // Assert - Assert.Equal(FileResultExecutorBase.PreconditionState.ShouldProcess, shouldProcess); + Assert.Equal(FileResultExecutorBase.PreconditionState.ShouldProcess, state); } [Theory] [InlineData("\"NotEtag\"", null)] [InlineData("\"Etag\"", "\"Etag\"")] [InlineData(null, null)] - public void ComputeConditionalHeaders_ShouldNotProcess_PreconditionFailed(string ifMatch, string ifNoneMatch) + public void GetPreconditionState_ShouldNotProcess_PreconditionFailed(string ifMatch, string ifNoneMatch) { var actionContext = new ActionContext(); var httpContext = new DefaultHttpContext(); @@ -329,20 +329,20 @@ public void ComputeConditionalHeaders_ShouldNotProcess_PreconditionFailed(string httpRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; httpRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(2); actionContext.HttpContext = httpContext; - var shouldProcess = FileResultExecutorBase.GetPreconditionState( + var state = FileResultExecutorBase.GetPreconditionState( actionContext, httpRequestHeaders, lastModified, etag); // Assert - Assert.Equal(FileResultExecutorBase.PreconditionState.PreconditionFailed, shouldProcess); + Assert.Equal(FileResultExecutorBase.PreconditionState.PreconditionFailed, state); } [Theory] [InlineData(null, "\"Etag\"")] [InlineData(null, null)] - public void ComputeConditionalHeaders_ShouldNotProcess_NotModified(string ifMatch, string ifNoneMatch) + public void GetPreconditionState_ShouldNotProcess_NotModified(string ifMatch, string ifNoneMatch) { var actionContext = new ActionContext(); var httpContext = new DefaultHttpContext(); @@ -363,14 +363,14 @@ public void ComputeConditionalHeaders_ShouldNotProcess_NotModified(string ifMatc }; httpRequestHeaders.IfModifiedSince = lastModified; actionContext.HttpContext = httpContext; - var shouldProcess = FileResultExecutorBase.GetPreconditionState( + var state = FileResultExecutorBase.GetPreconditionState( actionContext, httpRequestHeaders, lastModified, etag); // Assert - Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, shouldProcess); + Assert.Equal(FileResultExecutorBase.PreconditionState.NotModified, state); } 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 7e3d0a52f3..dda8daf11c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/FileStreamResultTest.cs @@ -127,6 +127,99 @@ public async Task WriteFileAsync_PreconditionStateShouldProcess_WritesRangeReque Assert.Equal(expectedString, body); } + [Fact] + public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() + { + // Arrange + var contentType = "text/plain"; + var lastModified = DateTimeOffset.MinValue; + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + readStream.SetLength(11); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + requestHeaders.Range = new RangeHeaderValue(0, 4); + requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + 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.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + var contentRange = new ContentRangeHeaderValue(0, 4, byteArray.Length); + Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Equal(5, httpResponse.ContentLength); + Assert.Equal("Hello", body); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedNotSatisfiable() + { + // Arrange + var contentType = "text/plain"; + var lastModified = DateTimeOffset.MinValue.AddDays(1); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + var byteArray = Encoding.ASCII.GetBytes("Hello World"); + var readStream = new MemoryStream(byteArray); + readStream.SetLength(11); + + var result = new FileStreamResult(readStream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + }; + + var httpContext = GetHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + requestHeaders.IfMatch = new[] + { + new EntityTagHeaderValue("\"Etag\""), + }; + requestHeaders.Range = new RangeHeaderValue(0, 4); + requestHeaders.IfRange = new RangeConditionHeaderValue(DateTimeOffset.MinValue); + httpContext.Request.Method = HttpMethods.Get; + httpContext.Response.Body = new MemoryStream(); + var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); + + // Act + await result.ExecuteResultAsync(actionContext); + + // Assert + var httpResponse = actionContext.HttpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = streamReader.ReadToEndAsync().Result; + var contentRange = new ContentRangeHeaderValue(byteArray.Length); + Assert.Equal(StatusCodes.Status416RangeNotSatisfiable, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(lastModified.ToString("R"), httpResponse.Headers[HeaderNames.LastModified]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Empty(body); + } + [Theory] [InlineData("0-5")] [InlineData("bytes = 11-0")] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs index 96bc3bd5e2..656cc5eea1 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/PhysicalFileResultTest.cs @@ -89,6 +89,68 @@ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, st Assert.Equal(expectedString, body); } + [Fact] + public async Task WriteFileAsync_IfRangeHeaderValid_WritesRequestedRange() + { + // Arrange + var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); + var result = new TestPhysicalFileResult(path, "text/plain"); + var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); + var httpContext = GetHttpContext(); + 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; + 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.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + var contentRange = new ContentRangeHeaderValue(0, 3, 34); + Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Equal(4, httpResponse.ContentLength); + Assert.Equal("File", body); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedNotSatisfiable() + { + // Arrange + var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); + var result = new TestPhysicalFileResult(path, "text/plain"); + var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\""); + var httpContext = GetHttpContext(); + 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; + 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.Status416RangeNotSatisfiable, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.Empty(body); + } + [Theory] [InlineData("0-5")] [InlineData("bytes = 11-0")] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs index b3e98dc638..e3995bb07b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/VirtualFileResultTest.cs @@ -102,6 +102,95 @@ public async Task WriteFileAsync_WritesRangeRequested(long? start, long? end, st Assert.Equal(expectedString, body); } + [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); + 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 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; + 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.Status206PartialContent, httpResponse.StatusCode); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + var contentRange = new ContentRangeHeaderValue(0, 3, 33); + Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Equal(4, httpResponse.ContentLength); + Assert.Equal("File", body); + } + + [Fact] + public async Task WriteFileAsync_IfRangeHeaderInvalid_RangeRequestedNotSatisfiable() + { + // 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 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; + 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.Status416RangeNotSatisfiable, httpResponse.StatusCode); + var contentRange = new ContentRangeHeaderValue(33); + Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]); + Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]); + Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]); + Assert.Empty(body); + } + [Theory] [InlineData("0-5")] [InlineData("bytes = 11-0")] diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs index 387fd9b437..be6e37d473 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/FileResultTests.cs @@ -149,6 +149,45 @@ public async Task FileFromDisk_ReturnsFileWithFileName() Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); } + [Fact] + public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest_WithLastModifiedAndEtag() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.NotNull(body); + Assert.Equal("This is", body); + } + + [Fact] + public async Task FileFromDisk_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestNotSatisfiable_WithLastModifiedAndEtag() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromDiskWithFileName_WithLastModifiedAndEtag"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\"")); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Empty(body); + } + [Fact] public async Task FileFromStream_ReturnsFile() { @@ -230,6 +269,45 @@ public async Task FileFromStream_ReturnsFileWithFileName() Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); } + [Fact] + public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.NotNull(body); + Assert.Equal("This is", body); + } + + [Fact] + public async Task FileFromStream_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestNotSatisfiable() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromStreamWithFileName_WithEtag"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\"")); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Empty(body); + } + [Fact] public async Task FileFromBinaryData_ReturnsFile() { @@ -311,6 +389,45 @@ public async Task FileFromBinaryData_ReturnsFileWithFileName() Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); } + [Fact] + public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.NotNull(body); + Assert.Equal("This is", body); + } + + [Fact] + public async Task FileFromBinaryData_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestNotSatisfiable() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/DownloadFiles/DownloadFromBinaryDataWithFileName_WithEtag"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\"")); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Empty(body); + } + [Fact] public async Task FileFromEmbeddedResources_ReturnsFileWithFileName() { @@ -360,6 +477,51 @@ public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_RangeRequest Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); } + [Fact] + public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderValid_RangeRequest() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_WithEtag"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\"")); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.NotNull(body); + Assert.Equal("Sample ", body); + var contentDisposition = response.Content.Headers.ContentDisposition.ToString(); + Assert.NotNull(contentDisposition); + Assert.Equal("attachment; filename=downloadName.txt; filename*=UTF-8''downloadName.txt", contentDisposition); + } + + [Fact] + public async Task FileFromEmbeddedResources_ReturnsFileWithFileName_IfRangeHeaderInvalid_RangeRequestotSatisfiable() + { + // Arrange + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://localhost/EmbeddedFiles/DownloadFileWithFileName_WithEtag"); + httpRequestMessage.Headers.Range = new RangeHeaderValue(0, 6); + httpRequestMessage.Headers.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\"")); + + // Act + var response = await Client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); + var body = await response.Content.ReadAsStringAsync(); + Assert.Empty(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("0-6")] [InlineData("bytes = 11-6")] diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs index d8985f0a9e..b6280158cb 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs @@ -36,7 +36,7 @@ public string Name var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "test.cshtml")); - var csharpDocument = RazorCSharpDocument.Create(content, Array.Empty()); + var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); // Act var result = compilationService.Compile(codeDocument, csharpDocument); @@ -57,7 +57,7 @@ public class MyTestType {}"; var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "test.cshtml")); - var csharpDocument = RazorCSharpDocument.Create(content, Array.Empty()); + var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); // Act var result = compilationService.Compile(codeDocument, csharpDocument); @@ -79,7 +79,7 @@ public void Compile_ReturnsCompilationFailureWithPathsFromLinePragmas() var compilationService = GetRoslynCompilationService(); var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath)); - var csharpDocument = RazorCSharpDocument.Create(content, Array.Empty()); + var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); // Act var result = compilationService.Compile(codeDocument, csharpDocument); @@ -103,7 +103,7 @@ public void Compile_ReturnsGeneratedCodePath_IfLinePragmaIsNotAvailable() var compilationService = GetRoslynCompilationService(); var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create(fileContent, viewPath)); - var csharpDocument = RazorCSharpDocument.Create(content, Array.Empty()); + var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); // Act var result = compilationService.Compile(codeDocument, csharpDocument); @@ -134,7 +134,7 @@ public class MyNonCustomDefinedClass {} var compilationService = GetRoslynCompilationService(options: options); var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", viewPath)); - var csharpDocument = RazorCSharpDocument.Create(content, Array.Empty()); + var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); // Act var result = compilationService.Compile(codeDocument, csharpDocument); @@ -237,7 +237,7 @@ public void Compile_RunsCallback() var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path")); - var csharpDocument = RazorCSharpDocument.Create(content, Array.Empty()); + var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); // Act var result = compilationService.Compile(codeDocument, csharpDocument); @@ -258,7 +258,7 @@ public void Compile_DoesNotThrowIfReferencesWereClearedInCallback() var compilationService = GetRoslynCompilationService(options: options); var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); - var csharpDocument = RazorCSharpDocument.Create(content, Array.Empty()); + var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); // Act var result = compilationService.Compile(codeDocument, csharpDocument); @@ -285,7 +285,7 @@ public void Compile_SucceedsIfReferencesAreAddedInCallback() var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("Hello world", "some-relative-path.cshtml")); - var csharpDocument = RazorCSharpDocument.Create(content, Array.Empty()); + var csharpDocument = RazorCSharpDocument.Create(content, RazorCodeGenerationOptions.CreateDefault(), Array.Empty()); // Act var result = compilationService.Compile(codeDocument, csharpDocument); diff --git a/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs b/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs index 0ef71143ac..e86e211932 100644 --- a/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs +++ b/test/WebSites/FilesWebSite/Controllers/DownloadFilesController.cs @@ -69,6 +69,17 @@ public IActionResult DownloadFromStreamWithFileName() return File(stream, "text/plain", "downloadName.txt"); } + public IActionResult DownloadFromStreamWithFileName_WithEtag() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write("This is sample text from a stream"); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + return File(stream, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag); + } + public IActionResult DownloadFromBinaryData() { var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array"); @@ -80,5 +91,12 @@ public IActionResult DownloadFromBinaryDataWithFileName() var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array"); return File(data, "text/plain", "downloadName.txt"); } + + public IActionResult DownloadFromBinaryDataWithFileName_WithEtag() + { + var data = Encoding.UTF8.GetBytes("This is a sample text from a binary array"); + var entityTag = new EntityTagHeaderValue("\"Etag\""); + return File(data, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag); + } } } diff --git a/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs b/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs index 5b471886b7..758aea7b7d 100644 --- a/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs +++ b/test/WebSites/FilesWebSite/Controllers/EmbeddedFilesController.cs @@ -17,5 +17,17 @@ public IActionResult DownloadFileWithFileName() FileDownloadName = "downloadName.txt" }; } + + public IActionResult DownloadFileWithFileName_WithEtag() + { + var file = new VirtualFileResult("/Greetings.txt", "text/plain") + { + FileProvider = new EmbeddedFileProvider(GetType().GetTypeInfo().Assembly, "FilesWebSite.EmbeddedResources"), + FileDownloadName = "downloadName.txt" + }; + + file.EntityTag = new Microsoft.Net.Http.Headers.EntityTagHeaderValue("\"Etag\""); + return file; + } } } \ No newline at end of file