From 3a0d8c43ca1cbfb96def9c2885418f96702e42d1 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Wed, 18 May 2016 16:10:06 -0700 Subject: [PATCH] Add more unit tests --- .../Infrastructure/RangeHelpers.cs | 4 + .../StaticFileContext.cs | 6 +- .../CacheHeaderTests.cs | 209 ++++++++++++++---- .../RangeHeaderTests.cs | 75 ++++++- .../StaticFileMiddlewareTests.cs | 156 ++++++++----- .../SubFolder/Empty.txt | 0 .../SubFolder/SingleByte.txt | 1 + 7 files changed, 349 insertions(+), 102 deletions(-) create mode 100644 test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/Empty.txt create mode 100644 test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/SingleByte.txt diff --git a/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/RangeHelpers.cs b/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/RangeHelpers.cs index e6cf50e025..2423018545 100644 --- a/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/RangeHelpers.cs +++ b/src/Microsoft.AspNetCore.StaticFiles/Infrastructure/RangeHelpers.cs @@ -16,6 +16,10 @@ namespace Microsoft.AspNetCore.StaticFiles.Infrastructure internal static IList NormalizeRanges(ICollection ranges, long length) { IList normalizedRanges = new List(ranges.Count); + if (length == 0) + { + return normalizedRanges; + } foreach (var range in ranges) { long? start = range.From; diff --git a/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs b/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs index 45a0b037a4..1e48e0e998 100644 --- a/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs +++ b/src/Microsoft.AspNetCore.StaticFiles/StaticFileContext.cs @@ -197,9 +197,11 @@ namespace Microsoft.AspNetCore.StaticFiles private void ComputeIfModifiedSince() { + var now = DateTimeOffset.UtcNow; + // 14.25 If-Modified-Since var ifModifiedSince = _requestHeaders.IfModifiedSince; - if (ifModifiedSince.HasValue) + if (ifModifiedSince.HasValue && ifModifiedSince <= now) { bool modified = ifModifiedSince < _lastModified; _ifModifiedSinceState = modified ? PreconditionState.ShouldProcess : PreconditionState.NotModified; @@ -207,7 +209,7 @@ namespace Microsoft.AspNetCore.StaticFiles // 14.28 If-Unmodified-Since var ifUnmodifiedSince = _requestHeaders.IfUnmodifiedSince; - if (ifUnmodifiedSince.HasValue) + if (ifUnmodifiedSince.HasValue && ifModifiedSince <= now) { bool unmodified = ifUnmodifiedSince >= _lastModified; _ifUnmodifiedSinceState = unmodified ? PreconditionState.ShouldProcess : PreconditionState.PreconditionFailed; diff --git a/test/Microsoft.AspNetCore.StaticFiles.Tests/CacheHeaderTests.cs b/test/Microsoft.AspNetCore.StaticFiles.Tests/CacheHeaderTests.cs index 1d5b200511..6f9d8c9015 100644 --- a/test/Microsoft.AspNetCore.StaticFiles.Tests/CacheHeaderTests.cs +++ b/test/Microsoft.AspNetCore.StaticFiles.Tests/CacheHeaderTests.cs @@ -2,6 +2,8 @@ // 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.Globalization; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -41,38 +43,52 @@ namespace Microsoft.AspNetCore.StaticFiles // as PUT, from modifying a resource that has changed since the client // last retrieved it. - [Fact] - public async Task IfMatchShouldReturn412WhenNotListed() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task IfMatchShouldReturn412WhenNotListed(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); - var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/extra.xml"); + var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req.Headers.Add("If-Match", "\"fake\""); HttpResponseMessage resp = await server.CreateClient().SendAsync(req); Assert.Equal(HttpStatusCode.PreconditionFailed, resp.StatusCode); } - [Fact] - public async Task IfMatchShouldBeServedWhenListed() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task IfMatchShouldBeServedWhenListed(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); - var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/extra.xml"); + var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req.Headers.Add("If-Match", original.Headers.ETag.ToString()); HttpResponseMessage resp = await server.CreateClient().SendAsync(req); Assert.Equal(HttpStatusCode.OK, resp.StatusCode); } - [Fact] - public async Task IfMatchShouldBeServedForAstrisk() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task IfMatchShouldBeServedForAstrisk(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); - var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/extra.xml"); + var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req.Headers.Add("If-Match", "*"); HttpResponseMessage resp = await server.CreateClient().SendAsync(req); Assert.Equal(HttpStatusCode.OK, resp.StatusCode); } + [Theory] + [MemberData(nameof(UnsupportedMethods))] + public async Task IfMatchShouldBeIgnoredForUnsupportedMethods(HttpMethod method) + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); + req.Headers.Add("If-Match", "*"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode); + } + // 14.26 If-None-Match // If any of the entity tags match the entity tag of the entity that // would have been returned in the response to a similar GET request @@ -87,38 +103,43 @@ namespace Microsoft.AspNetCore.StaticFiles // matched. For all other request methods, the server MUST respond with // a status of 412 (Precondition Failed). - [Fact] - public async Task IfNoneMatchShouldReturn304ForMatchingOnGetAndHeadMethod() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task IfNoneMatchShouldReturn304ForMatching(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); - var req2 = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/extra.xml"); + var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req2.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString()); HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2); Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode); - - var req3 = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/extra.xml"); - req3.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString()); - HttpResponseMessage resp3 = await server.CreateClient().SendAsync(req3); - Assert.Equal(HttpStatusCode.NotModified, resp3.StatusCode); } - [Fact] - public async Task IfNoneMatchShouldBeIgnoredForNonTwoHundredAnd304Responses() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task IfNoneMatchAllShouldReturn304ForMatching(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); - var req2 = new HttpRequestMessage(HttpMethod.Post, "http://localhost/SubFolder/extra.xml"); + var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); + req2.Headers.Add("If-None-Match", "*"); + HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2); + Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode); + } + + [Theory] + [MemberData(nameof(UnsupportedMethods))] + public async Task IfNoneMatchShouldBeIgnoredForNonTwoHundredAnd304Responses(HttpMethod method) + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + HttpResponseMessage resp1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); + + var req2 = new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml"); req2.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString()); HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2); Assert.Equal(HttpStatusCode.NotFound, resp2.StatusCode); - - var req3 = new HttpRequestMessage(HttpMethod.Put, "http://localhost/SubFolder/extra.xml"); - req3.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString()); - HttpResponseMessage resp3 = await server.CreateClient().SendAsync(req3); - Assert.Equal(HttpStatusCode.NotFound, resp3.StatusCode); } // 14.26 If-None-Match @@ -131,12 +152,15 @@ namespace Microsoft.AspNetCore.StaticFiles // A server MUST use the strong comparison function (see section 13.3.3) // to compare the entity tags in If-Match. - [Fact] - public async Task ServerShouldReturnLastModified() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task ServerShouldReturnLastModified(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); - HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/SubFolder/extra.xml"); + HttpResponseMessage response = await server.CreateClient().SendAsync( + new HttpRequestMessage(method, "http://localhost/SubFolder/extra.xml")); + Assert.NotNull(response.Content.Headers.LastModified); // Verify that DateTimeOffset is UTC Assert.Equal(response.Content.Headers.LastModified.Value.Offset, TimeSpan.Zero); @@ -151,30 +175,58 @@ namespace Microsoft.AspNetCore.StaticFiles // unless doing so is consistent with all of the conditional header // fields in the request. - [Fact] - public async Task MatchingBothConditionsReturnsNotModified() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task MatchingBothConditionsReturnsNotModified(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server .CreateRequest("/SubFolder/extra.xml") - .GetAsync(); + .SendAsync(method.Method); HttpResponseMessage resp2 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-None-Match", resp1.Headers.ETag.ToString()) .And(req => req.Headers.IfModifiedSince = resp1.Content.Headers.LastModified) - .GetAsync(); + .SendAsync(method.Method); Assert.Equal(HttpStatusCode.NotModified, resp2.StatusCode); } - [Fact] - public async Task MissingEitherOrBothConditionsReturnsNormally() + + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task MatchingAtLeastOneETagReturnsNotModified(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage resp1 = await server .CreateRequest("/SubFolder/extra.xml") - .GetAsync(); + .SendAsync(method.Method); + var etag = resp1.Headers.ETag.ToString(); + + HttpResponseMessage resp2 = await server + .CreateRequest("/SubFolder/extra.xml") + .AddHeader("If-Match", etag + ", " + etag) + .SendAsync(method.Method); + + Assert.Equal(HttpStatusCode.OK, resp2.StatusCode); + + HttpResponseMessage resp3 = await server + .CreateRequest("/SubFolder/extra.xml") + .AddHeader("If-Match", etag+ ", \"badetag\"") + .SendAsync(method.Method); + + Assert.Equal(HttpStatusCode.OK, resp3.StatusCode); + } + + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task MissingEitherOrBothConditionsReturnsNormally(HttpMethod method) + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + HttpResponseMessage resp1 = await server + .CreateRequest("/SubFolder/extra.xml") + .SendAsync(method.Method); DateTimeOffset lastModified = resp1.Content.Headers.LastModified.Value; DateTimeOffset pastDate = lastModified.AddHours(-1); @@ -184,19 +236,19 @@ namespace Microsoft.AspNetCore.StaticFiles .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-None-Match", "\"fake\"") .And(req => req.Headers.IfModifiedSince = lastModified) - .GetAsync(); + .SendAsync(method.Method); HttpResponseMessage resp3 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-None-Match", resp1.Headers.ETag.ToString()) .And(req => req.Headers.IfModifiedSince = pastDate) - .GetAsync(); + .SendAsync(method.Method); HttpResponseMessage resp4 = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-None-Match", "\"fake\"") .And(req => req.Headers.IfModifiedSince = furtureDate) - .GetAsync(); + .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, resp2.StatusCode); Assert.Equal(HttpStatusCode.OK, resp3.StatusCode); @@ -215,15 +267,30 @@ namespace Microsoft.AspNetCore.StaticFiles // invalid, the response is exactly the same as for a normal GET. // A date which is later than the server's current time is // invalid. - [Fact] - public async Task InvalidIfModifiedSinceDateFormatGivesNormalGet() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task InvalidIfModifiedSinceDateFormatGivesNormalGet(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res = await server .CreateRequest("/SubFolder/extra.xml") .AddHeader("If-Modified-Since", "bad-date") - .GetAsync(); + .SendAsync(method.Method); + + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + } + + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task FutureIfModifiedSinceDateFormatGivesNormalGet(HttpMethod method) + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + + HttpResponseMessage res = await server + .CreateRequest("/SubFolder/extra.xml") + .And(req => req.Headers.IfModifiedSince = DateTimeOffset.Now.AddYears(1)) + .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, res.StatusCode); } @@ -235,38 +302,82 @@ namespace Microsoft.AspNetCore.StaticFiles // Modified-Since date, the server SHOULD return a 304 (Not // Modified) response. - [Fact] - public async Task IfModifiedSinceDateGreaterThanLastModifiedShouldReturn304() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task IfModifiedSinceDateGreaterThanLastModifiedShouldReturn304(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res1 = await server .CreateRequest("/SubFolder/extra.xml") - .GetAsync(); + .SendAsync(method.Method); HttpResponseMessage res2 = await server .CreateRequest("/SubFolder/extra.xml") .And(req => req.Headers.IfModifiedSince = DateTimeOffset.Now) - .GetAsync(); + .SendAsync(method.Method); Assert.Equal(HttpStatusCode.NotModified, res2.StatusCode); } - [Fact] - public async Task IfModifiedSinceDateLessThanLastModifiedShouldReturn200() + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task SuppportsIfModifiedDateFormats(HttpMethod method) + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + HttpResponseMessage res1 = await server + .CreateRequest("/SubFolder/extra.xml") + .SendAsync(method.Method); + + var formats = new[] + { + "ddd, dd MMM yyyy HH:mm:ss 'GMT'", + "dddd, dd-MMM-yy HH:mm:ss 'GMT'", + "ddd MMM d HH:mm:ss yyyy" + }; + + foreach (var format in formats) + { + HttpResponseMessage res2 = await server + .CreateRequest("/SubFolder/extra.xml") + .AddHeader("If-Modified-Since", DateTimeOffset.Now.ToString(format)) + .SendAsync(method.Method); + + Assert.Equal(HttpStatusCode.NotModified, res2.StatusCode); + } + } + + [Theory] + [MemberData(nameof(SupportedMethods))] + public async Task IfModifiedSinceDateLessThanLastModifiedShouldReturn200(HttpMethod method) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); HttpResponseMessage res1 = await server .CreateRequest("/SubFolder/extra.xml") - .GetAsync(); + .SendAsync(method.Method); HttpResponseMessage res2 = await server .CreateRequest("/SubFolder/extra.xml") .And(req => req.Headers.IfModifiedSince = DateTimeOffset.MinValue) - .GetAsync(); + .SendAsync(method.Method); Assert.Equal(HttpStatusCode.OK, res2.StatusCode); } + + public static IEnumerable SupportedMethods => new[] + { + new [] { HttpMethod.Get }, + new [] { HttpMethod.Head } + }; + + public static IEnumerable UnsupportedMethods => new[] + { + new [] { HttpMethod.Post }, + new [] { HttpMethod.Put }, + new [] { HttpMethod.Options }, + new [] { HttpMethod.Trace }, + new [] { new HttpMethod("VERB") } + }; } } diff --git a/test/Microsoft.AspNetCore.StaticFiles.Tests/RangeHeaderTests.cs b/test/Microsoft.AspNetCore.StaticFiles.Tests/RangeHeaderTests.cs index 16ad4143ff..71e1d7274d 100644 --- a/test/Microsoft.AspNetCore.StaticFiles.Tests/RangeHeaderTests.cs +++ b/test/Microsoft.AspNetCore.StaticFiles.Tests/RangeHeaderTests.cs @@ -72,6 +72,35 @@ namespace Microsoft.AspNetCore.StaticFiles Assert.Equal("0123456789a", await resp.Content.ReadAsStringAsync()); } + [Fact] + public async Task IfModifiedSinceWithPastDateShouldServePartialContent() + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt"); + + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt"); + req.Headers.Add("If-Modified-Since", original.Content.Headers.LastModified.Value.AddHours(-1).ToString("r")); + req.Headers.Add("Range", "bytes=0-10"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.PartialContent, resp.StatusCode); + Assert.Equal("bytes 0-10/62", resp.Content.Headers.ContentRange.ToString()); + Assert.Equal(11, resp.Content.Headers.ContentLength); + Assert.Equal("0123456789a", await resp.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task IfModifiedSinceWithCurrentDateShouldReturn304() + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/ranges.txt"); + + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/ranges.txt"); + req.Headers.Add("If-Modified-Since", original.Content.Headers.LastModified.Value.ToString("r")); + req.Headers.Add("Range", "bytes=0-10"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.NotModified, resp.StatusCode); + } + // 14.27 If-Range // If the client has no entity tag for an entity, but does have a Last- Modified date, it MAY use that date in an If-Range header. // HEAD requests should ignore the Range header @@ -216,7 +245,9 @@ namespace Microsoft.AspNetCore.StaticFiles // 14.35 Range [Theory] [InlineData("0-0", "0-0", 1, "0")] - [InlineData("0-9", "0-9", 10, "0123456789")] + [InlineData("0- 9", "0-9", 10, "0123456789")] + [InlineData("0 -9", "0-9", 10, "0123456789")] + [InlineData("0 - 9", "0-9", 10, "0123456789")] [InlineData("10-35", "10-35", 26, "abcdefghijklmnopqrstuvwxyz")] [InlineData("36-61", "36-61", 26, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")] [InlineData("36-", "36-61", 26, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")] // Last 26 @@ -236,6 +267,42 @@ namespace Microsoft.AspNetCore.StaticFiles Assert.Equal(expectedData, await resp.Content.ReadAsStringAsync()); } + [Theory] + [InlineData("0-0", "0-0", 1, "A")] + [InlineData("0-", "0-0", 1, "A")] + [InlineData("-1", "0-0", 1, "A")] + [InlineData("-2", "0-0", 1, "A")] + [InlineData("0-1", "0-0", 1, "A")] + [InlineData("0-2", "0-0", 1, "A")] + public async Task SingleValidRangeShouldServePartialContentSingleByteFile(string range, string expectedRange, int length, string expectedData) + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/SingleByte.txt"); + req.Headers.Add("Range", "bytes=" + range); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.PartialContent, resp.StatusCode); + Assert.NotNull(resp.Content.Headers.ContentRange); + Assert.Equal("bytes " + expectedRange + "/1", resp.Content.Headers.ContentRange.ToString()); + Assert.Equal(length, resp.Content.Headers.ContentLength); + Assert.Equal(expectedData, await resp.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("0-0")] + [InlineData("0-")] + [InlineData("-1")] + [InlineData("-2")] + [InlineData("0-1")] + [InlineData("0-2")] + public async Task SingleValidRangeShouldServeRequestedRangeNotSatisfiableEmptyFile(string range) + { + TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Empty.txt"); + req.Headers.Add("Range", "bytes=" + range); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, resp.StatusCode); + } + // 14.35 Range // HEAD ignores range headers [Theory] @@ -287,6 +354,9 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("0")] [InlineData("1-0")] [InlineData("-")] + [InlineData("a-")] + [InlineData("-b")] + [InlineData("a-b")] public async Task SingleInvalidRangeIgnored(string range) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); @@ -305,6 +375,9 @@ namespace Microsoft.AspNetCore.StaticFiles [InlineData("0")] [InlineData("1-0")] [InlineData("-")] + [InlineData("a-")] + [InlineData("-b")] + [InlineData("a-b")] public async Task HEADSingleInvalidRangeIgnored(string range) { TestServer server = StaticFilesTestServer.Create(app => app.UseFileServer()); diff --git a/test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs b/test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs index 35d94d8cec..32a494098c 100644 --- a/test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.StaticFiles.Tests/StaticFileMiddlewareTests.cs @@ -2,7 +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.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -29,6 +31,24 @@ namespace Microsoft.AspNetCore.StaticFiles Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + public async Task FoundFile_LastModifiedTrimsSeconds() + { + using (var fileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory())) + { + var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions + { + FileProvider = fileProvider + })); + var fileInfo = fileProvider.GetFileInfo("TestDocument.txt"); + var response = await server.CreateRequest("TestDocument.txt").GetAsync(); + + var last = fileInfo.LastModified; + var trimed = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime(); + + Assert.Equal(response.Content.Headers.LastModified.Value, trimed); + } + } + [Fact] public async Task NullArguments() { @@ -45,30 +65,7 @@ namespace Microsoft.AspNetCore.StaticFiles } [Theory] - [InlineData("", @".", "/missing.file")] - [InlineData("/subdir", @".", "/subdir/missing.file")] - [InlineData("/missing.file", @"./", "/missing.file")] - [InlineData("", @"./", "/xunit.xml")] - public async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) - { - using (var fileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), baseDir))) - { - var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions - { - RequestPath = new PathString(baseUrl), - FileProvider = fileProvider - })); - var response = await server.CreateRequest(requestUrl).GetAsync(); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - } - - [Theory] - [InlineData("", @".", "/TestDocument.txt")] - [InlineData("/somedir", @".", "/somedir/TestDocument.txt")] - [InlineData("/SomeDir", @".", "/soMediR/TestDocument.txt")] - [InlineData("", @"SubFolder", "/ranges.txt")] - [InlineData("/somedir", @"SubFolder", "/somedir/ranges.txt")] + [MemberData(nameof(ExistingFiles))] public async Task FoundFile_Served_All(string baseUrl, string baseDir, string requestUrl) { await FoundFile_Served(baseUrl, baseDir, requestUrl); @@ -95,41 +92,26 @@ namespace Microsoft.AspNetCore.StaticFiles RequestPath = new PathString(baseUrl), FileProvider = fileProvider })); + var fileInfo = fileProvider.GetFileInfo(Path.GetFileName(requestUrl)); var response = await server.CreateRequest(requestUrl).GetAsync(); + var responseContent = await response.Content.ReadAsByteArrayAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); - Assert.True(response.Content.Headers.ContentLength > 0); - Assert.Equal(response.Content.Headers.ContentLength, (await response.Content.ReadAsByteArrayAsync()).Length); - } - } + Assert.True(response.Content.Headers.ContentLength == fileInfo.Length); + Assert.Equal(response.Content.Headers.ContentLength, responseContent.Length); - [Theory] - [InlineData("", @".", "/TestDocument.txt")] - [InlineData("/somedir", @".", "/somedir/TestDocument.txt")] - [InlineData("/SomeDir", @".", "/soMediR/TestDocument.txt")] - [InlineData("", @"SubFolder", "/ranges.txt")] - [InlineData("/somedir", @"SubFolder", "/somedir/ranges.txt")] - public async Task PostFile_PassesThrough(string baseUrl, string baseDir, string requestUrl) - { - using (var fileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), baseDir))) - { - var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions + using (var stream = fileInfo.CreateReadStream()) { - RequestPath = new PathString(baseUrl), - FileProvider = fileProvider - })); - var response = await server.CreateRequest(requestUrl).PostAsync(); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + var fileContents = new byte[stream.Length]; + stream.Read(fileContents, 0, (int)stream.Length); + Assert.True(responseContent.SequenceEqual(fileContents)); + } } } [Theory] - [InlineData("", @".", "/TestDocument.txt")] - [InlineData("/somedir", @".", "/somedir/TestDocument.txt")] - [InlineData("/SomeDir", @".", "/soMediR/TestDocument.txt")] - [InlineData("", @"SubFolder", "/ranges.txt")] - [InlineData("/somedir", @"SubFolder", "/somedir/ranges.txt")] + [MemberData(nameof(ExistingFiles))] public async Task HeadFile_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl) { using (var fileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), baseDir))) @@ -139,13 +121,87 @@ namespace Microsoft.AspNetCore.StaticFiles RequestPath = new PathString(baseUrl), FileProvider = fileProvider })); + var fileInfo = fileProvider.GetFileInfo(Path.GetFileName(requestUrl)); var response = await server.CreateRequest(requestUrl).SendAsync("HEAD"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("text/plain", response.Content.Headers.ContentType.ToString()); - Assert.True(response.Content.Headers.ContentLength > 0); + Assert.True(response.Content.Headers.ContentLength == fileInfo.Length); Assert.Equal(0, (await response.Content.ReadAsByteArrayAsync()).Length); } } + + [Theory] + [MemberData(nameof(MissingFiles))] + public async Task Get_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) => + await PassesThrough("GET", baseUrl, baseDir, requestUrl); + + [Theory] + [MemberData(nameof(MissingFiles))] + public async Task Head_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) => + await PassesThrough("HEAD", baseUrl, baseDir, requestUrl); + + [Theory] + [MemberData(nameof(MissingFiles))] + public async Task Unknown_NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) => + await PassesThrough("VERB", baseUrl, baseDir, requestUrl); + + [Theory] + [MemberData(nameof(ExistingFiles))] + public async Task Options_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => + await PassesThrough("OPTIONS", baseUrl, baseDir, requestUrl); + + [Theory] + [MemberData(nameof(ExistingFiles))] + public async Task Trace_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => + await PassesThrough("TRACE", baseUrl, baseDir, requestUrl); + + [Theory] + [MemberData(nameof(ExistingFiles))] + public async Task Post_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => + await PassesThrough("POST", baseUrl, baseDir, requestUrl); + + [Theory] + [MemberData(nameof(ExistingFiles))] + public async Task Put_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => + await PassesThrough("PUT", baseUrl, baseDir, requestUrl); + + [Theory] + [MemberData(nameof(ExistingFiles))] + public async Task Unknown_Match_PassesThrough(string baseUrl, string baseDir, string requestUrl) => + await PassesThrough("VERB", baseUrl, baseDir, requestUrl); + + public async Task PassesThrough(string method, string baseUrl, string baseDir, string requestUrl) + { + using (var fileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), baseDir))) + { + var server = StaticFilesTestServer.Create(app => app.UseStaticFiles(new StaticFileOptions + { + RequestPath = new PathString(baseUrl), + FileProvider = fileProvider + })); + var response = await server.CreateRequest(requestUrl).SendAsync(method); + Assert.Null(response.Content.Headers.LastModified); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + } + + public static IEnumerable MissingFiles => new[] + { + new[] {"", @".", "/missing.file"}, + new[] {"/subdir", @".", "/subdir/missing.file"}, + new[] {"/missing.file", @"./", "/missing.file"}, + new[] {"", @"./", "/xunit.xml"} + }; + + public static IEnumerable ExistingFiles => new[] + { + new[] {"", @".", "/TestDocument.txt"}, + new[] {"/somedir", @".", "/somedir/TestDocument.txt"}, + new[] {"/SomeDir", @".", "/soMediR/TestDocument.txt"}, + new[] {"", @"SubFolder", "/ranges.txt"}, + new[] {"/somedir", @"SubFolder", "/somedir/ranges.txt"}, + new[] {"", @"SubFolder", "/Empty.txt"} + }; } } diff --git a/test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/Empty.txt b/test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/Empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/SingleByte.txt b/test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/SingleByte.txt new file mode 100644 index 0000000000..8c7e5a667f --- /dev/null +++ b/test/Microsoft.AspNetCore.StaticFiles.Tests/SubFolder/SingleByte.txt @@ -0,0 +1 @@ +A \ No newline at end of file