From 2c4811f20141f938e608a8bd2714d8e21ab676fd Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Mon, 4 Aug 2014 16:11:28 -0700 Subject: [PATCH] Import static file tests from Katana. --- StaticFiles.sln | 63 +-- .../DefaultFilesMiddleware.cs | 2 +- .../DirectoryBrowserMiddleware.cs | 2 +- .../CacheHeaderTests.cs | 253 ++++++++++++ .../DefaultContentTypeProviderTests.cs | 66 ++++ .../DefaultFilesMiddlewareTests.cs | 118 ++++++ .../DirectoryBrowserMiddlewareTests.cs | 135 +++++++ .../Microsoft.AspNet.StaticFiles.Tests.kproj | 42 ++ .../Project.json | 21 + .../RangeHeaderTests.cs | 359 ++++++++++++++++++ .../SendFileResponseExtensionsTests.cs | 64 ++++ .../StaticFileMiddlewareTests.cs | 117 ++++++ .../SubFolder/Default.html | 11 + .../SubFolder/Extra.xml | 1 + .../SubFolder/Ranges.txt | 1 + .../TestDocument.txt | 1 + 16 files changed, 1232 insertions(+), 24 deletions(-) create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/CacheHeaderTests.cs create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/DefaultContentTypeProviderTests.cs create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/DefaultFilesMiddlewareTests.cs create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/DirectoryBrowserMiddlewareTests.cs create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/Microsoft.AspNet.StaticFiles.Tests.kproj create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/Project.json create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/RangeHeaderTests.cs create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/SendFileResponseExtensionsTests.cs create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/StaticFileMiddlewareTests.cs create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Default.html create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Extra.xml create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Ranges.txt create mode 100644 test/Microsoft.AspNet.StaticFiles.Tests/TestDocument.txt diff --git a/StaticFiles.sln b/StaticFiles.sln index e25e61ad53..b11788a58a 100644 --- a/StaticFiles.sln +++ b/StaticFiles.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.21628.1 +VisualStudioVersion = 14.0.21916.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{40EE0889-960E-41B4-A3D3-9CE963EB0797}" EndProject @@ -11,6 +11,15 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.StaticFile EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "StaticFileSample", "samples\StaticFileSample\StaticFileSample.kproj", "{092141D9-305A-4FC5-AE74-CB23982CA8D4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{EF02AFE8-7C15-4DDB-8B2C-58A676112A98}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.StaticFiles.Tests", "test\Microsoft.AspNet.StaticFiles.Tests\Microsoft.AspNet.StaticFiles.Tests.kproj", "{CC87FE7D-8F42-4BE9-A152-9625E837C1E5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5EE39BF7-6457-432B-B26B-53B77A1C03D9}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,27 +30,36 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Any CPU.ActiveCfg = Debug|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Any CPU.Build.0 = Debug|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|x86.ActiveCfg = Debug|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|x86.Build.0 = Debug|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Any CPU.ActiveCfg = Release|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Mixed Platforms.Build.0 = Release|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|x86.ActiveCfg = Release|x86 - {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|x86.Build.0 = Release|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Any CPU.ActiveCfg = Debug|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Any CPU.Build.0 = Debug|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|x86.ActiveCfg = Debug|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|x86.Build.0 = Debug|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Any CPU.ActiveCfg = Release|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Mixed Platforms.Build.0 = Release|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|x86.ActiveCfg = Release|x86 - {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|x86.Build.0 = Release|x86 + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Debug|x86.ActiveCfg = Debug|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Any CPU.Build.0 = Release|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {8D7BC5A4-F19C-4184-8338-A6B42997218C}.Release|x86.ActiveCfg = Release|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Any CPU.Build.0 = Release|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {092141D9-305A-4FC5-AE74-CB23982CA8D4}.Release|x86.ActiveCfg = Release|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Any CPU.Build.0 = Release|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -49,5 +67,6 @@ Global GlobalSection(NestedProjects) = preSolution {8D7BC5A4-F19C-4184-8338-A6B42997218C} = {40EE0889-960E-41B4-A3D3-9CE963EB0797} {092141D9-305A-4FC5-AE74-CB23982CA8D4} = {8B21A3A9-9CA6-4857-A6E0-1A3203404B60} + {CC87FE7D-8F42-4BE9-A152-9625E837C1E5} = {EF02AFE8-7C15-4DDB-8B2C-58A676112A98} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs index 98cf3bcf9b..96f6198b07 100644 --- a/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs +++ b/src/Microsoft.AspNet.StaticFiles/DefaultFilesMiddleware.cs @@ -74,7 +74,7 @@ namespace Microsoft.AspNet.StaticFiles if (!Helpers.PathEndsInSlash(context.Request.Path)) { context.Response.StatusCode = 301; - context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/"; + context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString; return Constants.CompletedTask; } diff --git a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs index cd6c7b830e..ba4b336163 100644 --- a/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs +++ b/src/Microsoft.AspNet.StaticFiles/DirectoryBrowserMiddleware.cs @@ -67,7 +67,7 @@ namespace Microsoft.AspNet.StaticFiles if (!Helpers.PathEndsInSlash(context.Request.Path)) { context.Response.StatusCode = 301; - context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/"; + context.Response.Headers[Constants.Location] = context.Request.PathBase + context.Request.Path + "/" + context.Request.QueryString; return Constants.CompletedTask; } diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/CacheHeaderTests.cs b/test/Microsoft.AspNet.StaticFiles.Tests/CacheHeaderTests.cs new file mode 100644 index 0000000000..894b91e622 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/CacheHeaderTests.cs @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.StaticFiles +{ + public class CacheHeaderTests + { + [Fact] + public async Task ServerShouldReturnETag() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/SubFolder/Extra.xml"); + response.Headers.ETag.ShouldNotBe(null); + response.Headers.ETag.Tag.ShouldNotBe(null); + } + + [Fact] + public async Task SameETagShouldBeReturnedAgain() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + + HttpResponseMessage response1 = await server.CreateClient().GetAsync("http://localhost/SubFolder/Extra.xml"); + HttpResponseMessage response2 = await server.CreateClient().GetAsync("http://localhost/SubFolder/Extra.xml"); + response1.Headers.ETag.ShouldBe(response2.Headers.ETag); + } + + // 14.24 If-Match + // If none of the entity tags match, or if "*" is given and no current + // entity exists, the server MUST NOT perform the requested method, and + // MUST return a 412 (Precondition Failed) response. This behavior is + // most useful when the client wants to prevent an updating method, such + // as PUT, from modifying a resource that has changed since the client + // last retrieved it. + + [Fact] + public async Task IfMatchShouldReturn412WhenNotListed() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Extra.xml"); + req.Headers.Add("If-Match", "\"fake\""); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + resp.StatusCode.ShouldBe(HttpStatusCode.PreconditionFailed); + } + + [Fact] + public async Task IfMatchShouldBeServedWhenListed() + { + TestServer server = TestServer.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"); + req.Headers.Add("If-Match", original.Headers.ETag.ToString()); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + resp.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task IfMatchShouldBeServedForAstrisk() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Extra.xml"); + req.Headers.Add("If-Match", "*"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + resp.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + // 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 + // (without the If-None-Match header) on that resource, or if "*" is + // given and any current entity exists for that resource, then the + // server MUST NOT perform the requested method, unless required to do + // so because the resource's modification date fails to match that + // supplied in an If-Modified-Since header field in the request. + // Instead, if the request method was GET or HEAD, the server SHOULD + // respond with a 304 (Not Modified) response, including the cache- + // related header fields (particularly ETag) of one of the entities that + // matched. For all other request methods, the server MUST respond with + // a status of 412 (Precondition Failed). + + [Fact] + public async Task IfNoneMatchShouldReturn304ForMatchingOnGetAndHeadMethod() + { + TestServer server = TestServer.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"); + req2.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString()); + HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2); + resp2.StatusCode.ShouldBe(HttpStatusCode.NotModified); + + 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); + resp3.StatusCode.ShouldBe(HttpStatusCode.NotModified); + } + + [Fact] + public async Task IfNoneMatchShouldBeIgnoredForNonTwoHundredAnd304Responses() + { + TestServer server = TestServer.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"); + req2.Headers.Add("If-None-Match", resp1.Headers.ETag.ToString()); + HttpResponseMessage resp2 = await server.CreateClient().SendAsync(req2); + resp2.StatusCode.ShouldBe(HttpStatusCode.NotFound); + + 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); + resp3.StatusCode.ShouldBe(HttpStatusCode.NotFound); + } + + // 14.26 If-None-Match + // If none of the entity tags match, then the server MAY perform the + // requested method as if the If-None-Match header field did not exist, + // but MUST also ignore any If-Modified-Since header field(s) in the + // request. That is, if no entity tags match, then the server MUST NOT + // return a 304 (Not Modified) response. + + // 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() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + + HttpResponseMessage response = await server.CreateClient().GetAsync("http://localhost/SubFolder/Extra.xml"); + response.Content.Headers.LastModified.ShouldNotBe(null); + } + + // 13.3.4 + // An HTTP/1.1 origin server, upon receiving a conditional request that + // includes both a Last-Modified date (e.g., in an If-Modified-Since or + // If-Unmodified-Since header field) and one or more entity tags (e.g., + // in an If-Match, If-None-Match, or If-Range header field) as cache + // validators, MUST NOT return a response status of 304 (Not Modified) + // unless doing so is consistent with all of the conditional header + // fields in the request. + + [Fact] + public async Task MatchingBothConditionsReturnsNotModified() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + HttpResponseMessage resp1 = await server + .CreateRequest("/SubFolder/Extra.xml") + .GetAsync(); + + 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(); + + resp2.StatusCode.ShouldBe(HttpStatusCode.NotModified); + } + + [Fact] + public async Task MissingEitherOrBothConditionsReturnsNormally() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + HttpResponseMessage resp1 = await server + .CreateRequest("/SubFolder/Extra.xml") + .GetAsync(); + + DateTimeOffset lastModified = resp1.Content.Headers.LastModified.Value; + DateTimeOffset pastDate = lastModified.AddHours(-1); + DateTimeOffset furtureDate = lastModified.AddHours(1); + + HttpResponseMessage resp2 = await server + .CreateRequest("/SubFolder/Extra.xml") + .AddHeader("If-None-Match", "\"fake\"") + .And(req => req.Headers.IfModifiedSince = lastModified) + .GetAsync(); + + HttpResponseMessage resp3 = await server + .CreateRequest("/SubFolder/Extra.xml") + .AddHeader("If-None-Match", resp1.Headers.ETag.ToString()) + .And(req => req.Headers.IfModifiedSince = pastDate) + .GetAsync(); + + HttpResponseMessage resp4 = await server + .CreateRequest("/SubFolder/Extra.xml") + .AddHeader("If-None-Match", "\"fake\"") + .And(req => req.Headers.IfModifiedSince = furtureDate) + .GetAsync(); + + resp2.StatusCode.ShouldBe(HttpStatusCode.OK); + resp3.StatusCode.ShouldBe(HttpStatusCode.OK); + resp4.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + // 14.25 If-Modified-Since + // The If-Modified-Since request-header field is used with a method to + // make it conditional: if the requested variant has not been modified + // since the time specified in this field, an entity will not be + // returned from the server; instead, a 304 (not modified) response will + // be returned without any message-body. + + // a) If the request would normally result in anything other than a + // 200 (OK) status, or if the passed If-Modified-Since date is + // 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() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + + HttpResponseMessage res = await server + .CreateRequest("/SubFolder/Extra.xml") + .AddHeader("If-Modified-Since", "bad-date") + .GetAsync(); + + res.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + // b) If the variant has been modified since the If-Modified-Since + // date, the response is exactly the same as for a normal GET. + + // c) If the variant has not been modified since a valid If- + // Modified-Since date, the server SHOULD return a 304 (Not + // Modified) response. + + [Fact] + public async Task IfModifiedSinceDateEqualsLastModifiedShouldReturn304() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + + HttpResponseMessage res1 = await server + .CreateRequest("/SubFolder/Extra.xml") + .GetAsync(); + + HttpResponseMessage res2 = await server + .CreateRequest("/SubFolder/Extra.xml") + .And(req => req.Headers.IfModifiedSince = res1.Content.Headers.LastModified) + .GetAsync(); + + res2.StatusCode.ShouldBe(HttpStatusCode.NotModified); + } + } +} diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/DefaultContentTypeProviderTests.cs b/test/Microsoft.AspNet.StaticFiles.Tests/DefaultContentTypeProviderTests.cs new file mode 100644 index 0000000000..374e14182b --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/DefaultContentTypeProviderTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.StaticFiles +{ + public class DefaultContentTypeProviderTests + { + [Fact] + public void UnknownExtensionsReturnFalse() + { + var provider = new FileExtensionContentTypeProvider(); + string contentType; + provider.TryGetContentType("unknown.ext", out contentType).ShouldBe(false); + } + + [Fact] + public void KnownExtensionsReturnTrye() + { + var provider = new FileExtensionContentTypeProvider(); + string contentType; + provider.TryGetContentType("known.txt", out contentType).ShouldBe(true); + contentType.ShouldBe("text/plain"); + } + + [Fact] + public void DoubleDottedExtensionsAreNotSupported() + { + var provider = new FileExtensionContentTypeProvider(); + string contentType; + provider.TryGetContentType("known.exe.config", out contentType).ShouldBe(false); + } + + [Fact] + public void DashedExtensionsShouldBeMatched() + { + var provider = new FileExtensionContentTypeProvider(); + string contentType; + provider.TryGetContentType("known.dvr-ms", out contentType).ShouldBe(true); + contentType.ShouldBe("video/x-ms-dvr"); + } + + [Fact] + public void BothSlashFormatsAreUnderstood() + { + var provider = new FileExtensionContentTypeProvider(); + string contentType; + provider.TryGetContentType(@"/first/example.txt", out contentType).ShouldBe(true); + contentType.ShouldBe("text/plain"); + provider.TryGetContentType(@"\second\example.txt", out contentType).ShouldBe(true); + contentType.ShouldBe("text/plain"); + } + + [Fact] + public void DotsInDirectoryAreIgnored() + { + var provider = new FileExtensionContentTypeProvider(); + string contentType; + provider.TryGetContentType(@"/first.css/example.txt", out contentType).ShouldBe(true); + contentType.ShouldBe("text/plain"); + provider.TryGetContentType(@"\second.css\example.txt", out contentType).ShouldBe(true); + contentType.ShouldBe("text/plain"); + } + } +} diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/DefaultFilesMiddlewareTests.cs b/test/Microsoft.AspNet.StaticFiles.Tests/DefaultFilesMiddlewareTests.cs new file mode 100644 index 0000000000..5cd549d8ba --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/DefaultFilesMiddlewareTests.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.FileSystems; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.PipelineCore; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.StaticFiles +{ + public class DefaultFilesMiddlewareTests + { + [Fact] + public async Task NullArguments() + { + Assert.Throws(() => TestServer.Create(app => app.UseDefaultFiles((DefaultFilesOptions)null))); + + // No exception, default provided + TestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions() { FileSystem = null })); + + // PathString(null) is OK. + TestServer server = TestServer.Create(app => app.UseDefaultFiles((string)null)); + var response = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("", @"", "/missing.dir")] + [InlineData("", @".", "/missing.dir/")] + [InlineData("/subdir", @".", "/subdir/missing.dir")] + [InlineData("/subdir", @"", "/subdir/missing.dir/")] + [InlineData("", @"\", "/missing.dir")] + public async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => + { + app.UseDefaultFiles(new DefaultFilesOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + }); + app.Run(context => context.Response.WriteAsync(context.Request.Path.Value)); + }); + + var response = await server.CreateClient().GetAsync(requestUrl); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(requestUrl, await response.Content.ReadAsStringAsync()); // Should not be modified + } + + [Theory] + [InlineData("", @"", "/SubFolder/")] + [InlineData("", @".", "/SubFolder/")] + [InlineData("", @".\", "/SubFolder/")] + [InlineData("", @"SubFolder", "/")] + [InlineData("", @".\SubFolder", "/")] + public async Task FoundDirectoryWithDefaultFile_PathModified(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => + { + app.UseDefaultFiles(new DefaultFilesOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + }); + app.Run(context => context.Response.WriteAsync(context.Request.Path.Value)); + }); + + var response = await server.CreateClient().GetAsync(requestUrl); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(requestUrl + "default.html", await response.Content.ReadAsStringAsync()); // Should be modified + } + + [Theory] + [InlineData("", @"", "/SubFolder", "")] + [InlineData("", @".", "/SubFolder", "")] + [InlineData("", @".\", "/SubFolder", "")] + [InlineData("", @".\", "/SubFolder", "?a=b")] + [InlineData("", @".\", "/SubFolder", "?a=b")] + [InlineData("", @".\", "/SubFolder", "?a=b")] + public async Task NearMatch_RedirectAddSlash(string baseUrl, string baseDir, string requestUrl, string queryString) + { + TestServer server = TestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl + queryString).GetAsync(); + + Assert.Equal(HttpStatusCode.Moved, response.StatusCode); + Assert.Equal(requestUrl + "/" + queryString, response.Headers.Location.ToString()); + Assert.Equal(0, (await response.Content.ReadAsByteArrayAsync()).Length); + } + + [Theory] + [InlineData("/SubFolder", @"\", "/SubFolder/")] + [InlineData("/SubFolder", @"", "/somedir/")] + [InlineData("", @".\SubFolder", "/")] + [InlineData("", @".\SubFolder\", "/")] + public async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => app.UseDefaultFiles(new DefaultFilesOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl).GetAsync(); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); // Passed through + } + } +} diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/DirectoryBrowserMiddlewareTests.cs b/test/Microsoft.AspNet.StaticFiles.Tests/DirectoryBrowserMiddlewareTests.cs new file mode 100644 index 0000000000..77042807ab --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/DirectoryBrowserMiddlewareTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.FileSystems; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.StaticFiles +{ + public class DirectoryBrowserMiddlewareTests + { + [Fact] + public async Task NullArguments() + { + Assert.Throws(() => TestServer.Create(app => app.UseDirectoryBrowser((DirectoryBrowserOptions)null))); + Assert.Throws(() => TestServer.Create(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions() { Formatter = null }))); + + // No exception, default provided + TestServer.Create(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions() { FileSystem = null })); + + // PathString(null) is OK. + TestServer server = TestServer.Create(app => app.UseDirectoryBrowser((string)null)); + var response = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Theory] + [InlineData("", @"", "/missing.dir")] + [InlineData("", @".", "/missing.dir/")] + [InlineData("/subdir", @".", "/subdir/missing.dir")] + [InlineData("/subdir", @"", "/subdir/missing.dir/")] + [InlineData("", @"\", "/missing.dir")] + public async Task NoMatch_PassesThrough(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl).GetAsync(); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("", @"", "/")] + [InlineData("", @".", "/")] + [InlineData("", @"", "/SubFolder/")] + [InlineData("", @".", "/SubFolder/")] + [InlineData("/somedir", @"", "/somedir/")] + [InlineData("/somedir", @"\", "/somedir/")] + [InlineData("/somedir", @".", "/somedir/subfolder/")] + public async Task FoundDirectory_Served(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl).GetAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString()); + Assert.True(response.Content.Headers.ContentLength > 0); + Assert.Equal(response.Content.Headers.ContentLength, (await response.Content.ReadAsByteArrayAsync()).Length); + } + + [Theory] + [InlineData("", @"", "/SubFolder", "")] + [InlineData("", @".", "/SubFolder", "")] + [InlineData("/somedir", @"", "/somedir", "")] + [InlineData("/somedir", @".", "/somedir/subfolder", "")] + [InlineData("", @"", "/SubFolder", "?a=b")] + [InlineData("", @".", "/SubFolder", "?a=b")] + [InlineData("/somedir", @"", "/somedir", "?a=b")] + [InlineData("/somedir", @".", "/somedir/subfolder", "?a=b")] + public async Task NearMatch_RedirectAddSlash(string baseUrl, string baseDir, string requestUrl, string queryString) + { + TestServer server = TestServer.Create(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl + queryString).GetAsync(); + + Assert.Equal(HttpStatusCode.Moved, response.StatusCode); + Assert.Equal(requestUrl + "/" + queryString, response.Headers.Location.ToString()); + Assert.Equal(0, (await response.Content.ReadAsByteArrayAsync()).Length); + } + + [Theory] + [InlineData("", @"", "/")] + [InlineData("", @".", "/")] + [InlineData("", @"", "/SubFolder/")] + [InlineData("", @".", "/SubFolder/")] + [InlineData("/somedir", @"", "/somedir/")] + [InlineData("/somedir", @".", "/somedir/subfolder/")] + public async Task PostDirectory_PassesThrough(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl).PostAsync(); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("", @"", "/")] + [InlineData("", @".", "/")] + [InlineData("", @"", "/SubFolder/")] + [InlineData("", @".", "/SubFolder/")] + [InlineData("/somedir", @"", "/somedir/")] + [InlineData("/somedir", @".", "/somedir/subfolder/")] + public async Task HeadDirectory_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => app.UseDirectoryBrowser(new DirectoryBrowserOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl).SendAsync("HEAD"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString()); + Assert.True(response.Content.Headers.ContentLength == 0); + Assert.Equal(0, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } +} diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/Microsoft.AspNet.StaticFiles.Tests.kproj b/test/Microsoft.AspNet.StaticFiles.Tests/Microsoft.AspNet.StaticFiles.Tests.kproj new file mode 100644 index 0000000000..1df80008a9 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/Microsoft.AspNet.StaticFiles.Tests.kproj @@ -0,0 +1,42 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + cc87fe7d-8f42-4be9-a152-9625e837c1e5 + Library + + + ConsoleDebugger + + + WebDebugger + + + + + + + 2.0 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/Project.json b/test/Microsoft.AspNet.StaticFiles.Tests/Project.json new file mode 100644 index 0000000000..97d09cf289 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/Project.json @@ -0,0 +1,21 @@ +{ + "dependencies": { + "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.AspNet.HttpFeature": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "", + "Microsoft.AspNet.TestHost": "1.0.0-*", + "Xunit.KRunner": "1.0.0-*" + }, + "commands": { + "test": "Xunit.KRunner" + }, + "frameworks": { + "net45": { + "dependencies": { + "Shouldly": "1.1.1.1", + "System.Runtime": "" + } + } + } +} diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/RangeHeaderTests.cs b/test/Microsoft.AspNet.StaticFiles.Tests/RangeHeaderTests.cs new file mode 100644 index 0000000000..4ba7095713 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/RangeHeaderTests.cs @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.StaticFiles +{ + public class RangeHeaderTests + { + // 14.27 If-Range + // If the entity tag given in the If-Range header matches the current entity tag for the entity, then the server SHOULD + // provide the specified sub-range of the entity using a 206 (Partial content) response. + [Fact] + public async Task IfRangeWithCurrentEtagShouldServePartialContent() + { + TestServer server = TestServer.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-Range", original.Headers.ETag.ToString()); + 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()); + } + + // 14.27 If-Range + // If the entity tag given in the If-Range header matches the current entity tag for the entity, then the server SHOULD + // provide the specified sub-range of the entity using a 206 (Partial content) response. + // HEAD requests should ignore the Range header + [Fact] + public async Task HEADIfRangeWithCurrentEtagShouldReturn200Ok() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/Ranges.txt"); + + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("If-Range", original.Headers.ETag.ToString()); + req.Headers.Add("Range", "bytes=0-10"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Equal(original.Headers.ETag, resp.Headers.ETag); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + } + + // 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. + [Fact] + public async Task IfRangeWithCurrentDateShouldServePartialContent() + { + TestServer server = TestServer.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-Range", original.Content.Headers.LastModified.Value.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()); + } + + // 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 + [Fact] + public async Task HEADIfRangeWithCurrentDateShouldReturn200Ok() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/Ranges.txt"); + + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.ToString("r")); + req.Headers.Add("Range", "bytes=0-10"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Equal(original.Content.Headers.LastModified, resp.Content.Headers.LastModified); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + } + + // 14.27 If-Range + // If the entity tag does not match, then the server SHOULD return the entire entity using a 200 (OK) response. + [Fact] + public async Task IfRangeWithOldEtagShouldServeFullContent() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("If-Range", "\"OldEtag\""); + req.Headers.Add("Range", "bytes=0-10"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync()); + } + + // 14.27 If-Range + // If the entity tag does not match, then the server SHOULD return the entire entity using a 200 (OK) response. + [Fact] + public async Task HEADIfRangeWithOldEtagShouldServeFullContent() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("If-Range", "\"OldEtag\""); + req.Headers.Add("Range", "bytes=0-10"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + } + + // 14.27 If-Range + // If the entity tag/date does not match, then the server SHOULD return the entire entity using a 200 (OK) response. + [Fact] + public async Task IfRangeWithOldDateShouldServeFullContent() + { + TestServer server = TestServer.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-Range", original.Content.Headers.LastModified.Value.Subtract(TimeSpan.FromDays(1)).ToString("r")); + req.Headers.Add("Range", "bytes=0-10"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync()); + } + + // 14.27 If-Range + // If the entity tag/date does not match, then the server SHOULD return the entire entity using a 200 (OK) response. + [Fact] + public async Task HEADIfRangeWithOldDateShouldServeFullContent() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/Ranges.txt"); + + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.Subtract(TimeSpan.FromDays(1)).ToString("r")); + req.Headers.Add("Range", "bytes=0-10"); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + } + + // 14.27 If-Range + // The If-Range header SHOULD only be used together with a Range header, and MUST be ignored if the request + // does not include a Range header, or if the server does not support the sub-range operation. + [Fact] + public async Task IfRangeWithoutRangeShouldServeFullContent() + { + TestServer server = TestServer.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-Range", original.Headers.ETag.ToString()); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync()); + + req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.ToString("r")); + resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync()); + } + + // 14.27 If-Range + // The If-Range header SHOULD only be used together with a Range header, and MUST be ignored if the request + // does not include a Range header, or if the server does not support the sub-range operation. + [Fact] + public async Task HEADIfRangeWithoutRangeShouldServeFullContent() + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + HttpResponseMessage original = await server.CreateClient().GetAsync("http://localhost/SubFolder/Ranges.txt"); + + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("If-Range", original.Headers.ETag.ToString()); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + + req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("If-Range", original.Content.Headers.LastModified.Value.ToString("r")); + resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + } + + // 14.35 Range + [Theory] + [InlineData("0-0", "0-0", 1, "0")] + [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 + [InlineData("-26", "36-61", 26, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")] // Last 26 + [InlineData("0-", "0-61", 62, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")] + [InlineData("-1001", "0-61", 62, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")] + public async Task SingleValidRangeShouldServePartialContent(string range, string expectedRange, int length, string expectedData) + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Ranges.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 + "/62", resp.Content.Headers.ContentRange.ToString()); + Assert.Equal(length, resp.Content.Headers.ContentLength); + Assert.Equal(expectedData, await resp.Content.ReadAsStringAsync()); + } + + // 14.35 Range + // HEAD ignores range headers + [Theory] + [InlineData("10-35")] + public async Task HEADSingleValidRangeShouldReturnOk(string range) + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("Range", "bytes=" + range); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + } + + // 14.35 Range + [Theory] + [InlineData("100-")] // Out of range + [InlineData("1000-1001")] // Out of range + [InlineData("-0")] // Suffix range must be non-zero + public async Task SingleNotSatisfiableRange(string range) + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.TryAddWithoutValidation("Range", "bytes=" + range); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.RequestedRangeNotSatisfiable, resp.StatusCode); + Assert.Equal("bytes */62", resp.Content.Headers.ContentRange.ToString()); + } + + // 14.35 Range + // HEAD ignores range headers + [Theory] + [InlineData("1000-1001")] // Out of range + public async Task HEADSingleNotSatisfiableRangeReturnsOk(string range) + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.TryAddWithoutValidation("Range", "bytes=" + range); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + } + + // 14.35 Range + [Theory] + [InlineData("")] + [InlineData("0")] + [InlineData("1-0")] + [InlineData("-")] + public async Task SingleInvalidRangeIgnored(string range) + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.TryAddWithoutValidation("Range", "bytes=" + range); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync()); + } + + // 14.35 Range + [Theory] + [InlineData("")] + [InlineData("0")] + [InlineData("1-0")] + [InlineData("-")] + public async Task HEADSingleInvalidRangeIgnored(string range) + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.TryAddWithoutValidation("Range", "bytes=" + range); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + } + + // 14.35 Range + [Theory] + [InlineData("0-0,2-2")] + [InlineData("0-0,60-")] + [InlineData("0-0,-2")] + [InlineData("2-2,0-0")] + [InlineData("0-0,2-2,4-4,6-6,8-8")] + [InlineData("0-0,6-6,8-8,2-2,4-4")] + public async Task MultipleValidRangesShouldServeFullContent(string ranges) + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Get, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("Range", "bytes=" + ranges); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Equal("text/plain", resp.Content.Headers.ContentType.ToString()); + Assert.Null(resp.Content.Headers.ContentRange); + Assert.Equal(62, resp.Content.Headers.ContentLength); + Assert.Equal("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", await resp.Content.ReadAsStringAsync()); + } + + // 14.35 Range + [Theory] + [InlineData("0-0,2-2")] + [InlineData("0-0,60-")] + [InlineData("0-0,-2")] + [InlineData("2-2,0-0")] // SHOULD send in the requested order. + public async Task HEADMultipleValidRangesShouldServeFullContent(string range) + { + TestServer server = TestServer.Create(app => app.UseFileServer()); + var req = new HttpRequestMessage(HttpMethod.Head, "http://localhost/SubFolder/Ranges.txt"); + req.Headers.Add("Range", "bytes=" + range); + HttpResponseMessage resp = await server.CreateClient().SendAsync(req); + Assert.Equal(HttpStatusCode.OK, resp.StatusCode); + Assert.Equal("text/plain", resp.Content.Headers.ContentType.ToString()); + Assert.Equal(string.Empty, await resp.Content.ReadAsStringAsync()); + } + } +} diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/SendFileResponseExtensionsTests.cs b/test/Microsoft.AspNet.StaticFiles.Tests/SendFileResponseExtensionsTests.cs new file mode 100644 index 0000000000..bd24a88bb0 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/SendFileResponseExtensionsTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.HttpFeature; +using Microsoft.AspNet.PipelineCore; +using Xunit; + +namespace Microsoft.AspNet.StaticFiles +{ + public class SendFileResponseExtensionsTests + { + [Fact] + public void SendFileSupport() + { + var context = new DefaultHttpContext(); + var response = context.Response; + Assert.False(response.SupportsSendFile()); + context.SetFeature(new FakeSendFileFeature()); + Assert.True(response.SupportsSendFile()); + } + + [Fact] + public Task SendFileWhenNotSupported() + { + var response = new DefaultHttpContext().Response; + return Assert.ThrowsAsync(() => response.SendFileAsync("foo")); + } + + [Fact] + public async Task SendFileWorks() + { + var context = new DefaultHttpContext(); + var response = context.Response; + var fakeFeature = new FakeSendFileFeature(); + context.SetFeature(fakeFeature); + + await response.SendFileAsync("bob", 1, 3, CancellationToken.None); + + Assert.Equal("bob", fakeFeature.name); + Assert.Equal(1, fakeFeature.offset); + Assert.Equal(3, fakeFeature.length); + Assert.Equal(CancellationToken.None, fakeFeature.token); + } + + private class FakeSendFileFeature : IHttpSendFileFeature + { + public string name = null; + public long offset = 0; + public long? length = null; + public CancellationToken token; + + public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + this.name = path; + this.offset = offset; + this.length = length; + this.token = cancellation; + return Task.FromResult(0); + } + } + } +} diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/StaticFileMiddlewareTests.cs b/test/Microsoft.AspNet.StaticFiles.Tests/StaticFileMiddlewareTests.cs new file mode 100644 index 0000000000..1983b435ff --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/StaticFileMiddlewareTests.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.FileSystems; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.StaticFiles +{ + public class StaticFileMiddlewareTests + { + [Fact] + public async Task NullArguments() + { + Assert.Throws(() => TestServer.Create(app => app.UseStaticFiles((StaticFileOptions)null))); + Assert.Throws(() => TestServer.Create(app => app.UseStaticFiles(new StaticFileOptions() { ContentTypeProvider = null }))); + + // No exception, default provided + TestServer.Create(app => app.UseStaticFiles(new StaticFileOptions() { FileSystem = null })); + + // PathString(null) is OK. + TestServer server = TestServer.Create(app => app.UseStaticFiles((string)null)); + var response = await server.CreateClient().GetAsync("/"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public void GivenDirDoesntExist_Throw() + { + Assert.Throws(() => TestServer.Create(app => app.UseStaticFiles("/ThisDirDoesntExist"))); + } + + [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) + { + TestServer server = TestServer.Create(app => app.UseStaticFiles(new StaticFileOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl).GetAsync(); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("", @".", "/TestDocument.txt")] + [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 FoundFile_Served(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => app.UseStaticFiles(new StaticFileOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl).GetAsync(); + + 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); + } + + [Theory] + [InlineData("", @".", "/TestDocument.txt")] + [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) + { + TestServer server = TestServer.Create(app => app.UseStaticFiles(new StaticFileOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage response = await server.CreateRequest(requestUrl).PostAsync(); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("", @".", "/TestDocument.txt")] + [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 HeadFile_HeadersButNotBodyServed(string baseUrl, string baseDir, string requestUrl) + { + TestServer server = TestServer.Create(app => app.UseStaticFiles(new StaticFileOptions() + { + RequestPath = new PathString(baseUrl), + FileSystem = new PhysicalFileSystem(baseDir) + })); + HttpResponseMessage 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.Equal(0, (await response.Content.ReadAsByteArrayAsync()).Length); + } + } +} diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Default.html b/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Default.html new file mode 100644 index 0000000000..4740d83682 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Default.html @@ -0,0 +1,11 @@ + + + + + + + + + Hello World + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Extra.xml b/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Extra.xml new file mode 100644 index 0000000000..856ef17b46 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Extra.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Ranges.txt b/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Ranges.txt new file mode 100644 index 0000000000..fb31ae6de8 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/SubFolder/Ranges.txt @@ -0,0 +1 @@ +0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \ No newline at end of file diff --git a/test/Microsoft.AspNet.StaticFiles.Tests/TestDocument.txt b/test/Microsoft.AspNet.StaticFiles.Tests/TestDocument.txt new file mode 100644 index 0000000000..fb31ae6de8 --- /dev/null +++ b/test/Microsoft.AspNet.StaticFiles.Tests/TestDocument.txt @@ -0,0 +1 @@ +0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \ No newline at end of file