// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc { public class PhysicalFileResultTest { [Fact] public void Constructor_SetsFileName() { // Arrange var path = Path.GetFullPath("helllo.txt"); // Act var result = new TestPhysicalFileResult(path, "text/plain"); // Assert Assert.Equal(path, result.FileName); } [Fact] public void Constructor_SetsContentTypeAndParameters() { // Arrange var path = Path.GetFullPath("helllo.txt"); var contentType = "text/plain; charset=us-ascii; p1=p1-value"; var expectedMediaType = contentType; // Act var result = new TestPhysicalFileResult(path, contentType); // Assert Assert.Equal(path, result.FileName); MediaTypeAssert.Equal(expectedMediaType, result.ContentType); } [Fact] public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent() { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); var httpContext = GetHttpContext(); httpContext.Response.Body = new MemoryStream(); var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act await result.ExecuteResultAsync(context); httpContext.Response.Body.Position = 0; // Assert Assert.NotNull(httpContext.Response.Body); var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync(); Assert.Equal("FilePathResultTestFile contents�", contents); } [Fact] public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent() { // Arrange var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); var sendFileMock = new Mock(); sendFileMock .Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None)) .Returns(Task.FromResult(0)); var httpContext = GetHttpContext(); httpContext.Features.Set(sendFileMock.Object); var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act await result.ExecuteResultAsync(context); // Assert sendFileMock.Verify(); } [Fact] public async Task ExecuteResultAsync_SetsSuppliedContentTypeAndEncoding() { // Arrange var expectedContentType = "text/foo; charset=us-ascii"; var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile_ASCII.txt")); var result = new TestPhysicalFileResult(path, expectedContentType) { IsAscii = true }; var httpContext = GetHttpContext(); var memoryStream = new MemoryStream(); httpContext.Response.Body = memoryStream; var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act await result.ExecuteResultAsync(context); // Assert var contents = Encoding.ASCII.GetString(memoryStream.ToArray()); Assert.Equal("FilePathResultTestFile contents ASCII encoded", contents); Assert.Equal(expectedContentType, httpContext.Response.ContentType); } [Fact] public async Task ExecuteResultAsync_WorksWithAbsolutePaths() { // Arrange var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt")); var result = new TestPhysicalFileResult(path, "text/plain"); var httpContext = GetHttpContext(); httpContext.Response.Body = new MemoryStream(); var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); // Act await result.ExecuteResultAsync(context); httpContext.Response.Body.Position = 0; // Assert Assert.NotNull(httpContext.Response.Body); var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync(); Assert.Equal("FilePathResultTestFile contents�", contents); } [Theory] [InlineData("FilePathResultTestFile.txt")] [InlineData("./FilePathResultTestFile.txt")] [InlineData(".\\FilePathResultTestFile.txt")] [InlineData("~/FilePathResultTestFile.txt")] [InlineData("..\\TestFiles/FilePathResultTestFile.txt")] [InlineData("..\\TestFiles\\FilePathResultTestFile.txt")] [InlineData("..\\TestFiles/SubFolder/SubFolderTestFile.txt")] [InlineData("..\\TestFiles\\SubFolder\\SubFolderTestFile.txt")] [InlineData("..\\TestFiles/SubFolder\\SubFolderTestFile.txt")] [InlineData("..\\TestFiles\\SubFolder/SubFolderTestFile.txt")] [InlineData("~/SubFolder/SubFolderTestFile.txt")] [InlineData("~/SubFolder\\SubFolderTestFile.txt")] public async Task ExecuteAsync_ThrowsNotSupported_ForNonRootedPaths(string path) { // Arrange var result = new TestPhysicalFileResult(path, "text/plain"); var context = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor()); var expectedMessage = $"Path '{path}' was not rooted."; // Act var ex = await Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); // Assert Assert.Equal(expectedMessage, ex.Message); } [Theory] [InlineData("/SubFolder/SubFolderTestFile.txt")] [InlineData("\\SubFolder\\SubFolderTestFile.txt")] [InlineData("/SubFolder\\SubFolderTestFile.txt")] [InlineData("\\SubFolder/SubFolderTestFile.txt")] [InlineData("./SubFolder/SubFolderTestFile.txt")] [InlineData(".\\SubFolder\\SubFolderTestFile.txt")] [InlineData("./SubFolder\\SubFolderTestFile.txt")] [InlineData(".\\SubFolder/SubFolderTestFile.txt")] public void ExecuteAsync_ThrowsDirectoryNotFound_IfItCanNotFindTheDirectory_ForRootPaths(string path) { // Arrange var result = new TestPhysicalFileResult(path, "text/plain"); var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); // Act & Assert Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); } [Theory] [InlineData("/FilePathResultTestFile.txt")] [InlineData("\\FilePathResultTestFile.txt")] public void ExecuteAsync_ThrowsFileNotFound_WhenFileDoesNotExist_ForRootPaths(string path) { // Arrange var result = new TestPhysicalFileResult(path, "text/plain"); var context = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); // Act & Assert Assert.ThrowsAsync(() => result.ExecuteResultAsync(context)); } private class TestPhysicalFileResult : PhysicalFileResult { public TestPhysicalFileResult(string filePath, string contentType) : base(filePath, contentType) { } public override Task ExecuteResultAsync(ActionContext context) { var executor = context.HttpContext.RequestServices.GetRequiredService(); executor.IsAscii = IsAscii; return executor.ExecuteAsync(context, this); } public bool IsAscii { get; set; } = false; } private class TestPhysicalFileResultExecutor : PhysicalFileResultExecutor { public TestPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory) { } public bool IsAscii { get; set; } = false; protected override Stream GetFileStream(string path) { if (IsAscii) { return new MemoryStream(Encoding.ASCII.GetBytes("FilePathResultTestFile contents ASCII encoded")); } else { return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents�")); } } } private static IServiceCollection CreateServices() { var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(NullLoggerFactory.Instance); return services; } private static HttpContext GetHttpContext() { var services = CreateServices(); var httpContext = new DefaultHttpContext(); httpContext.RequestServices = services.BuildServiceProvider(); return httpContext; } } }