|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
namespace Microsoft.AspNetCore.Mvc.Razor;
public class RazorViewEngineTest
{
private static readonly Dictionary<string, object> _areaTestContext = new Dictionary<string, object>()
{
{"area", "foo"},
{"controller", "bar"},
};
private static readonly Dictionary<string, object> _controllerTestContext = new Dictionary<string, object>()
{
{"controller", "bar"},
};
private static readonly Dictionary<string, object> _pageTestContext = new Dictionary<string, object>()
{
{"page", "/Accounts/Index"},
};
public static IEnumerable<object[]> AbsoluteViewPathData
{
get
{
yield return new[] { "~/foo/bar" };
yield return new[] { "/foo/bar" };
yield return new[] { "~/foo/bar.txt" };
yield return new[] { "/foo/bar.txt" };
}
}
[Theory]
[InlineData(null, "Value cannot be null.")]
[InlineData("", "The value cannot be an empty string.")]
public void FindView_IsMainPage_ThrowsIfViewNameIsNullOrEmpty(string viewName, string expectedMessage)
{
// Arrange
var viewEngine = CreateViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => viewEngine.FindView(context, viewName, isMainPage: true),
"viewName",
expectedMessage);
}
[Theory]
[MemberData(nameof(AbsoluteViewPathData))]
public void FindView_IsMainPage_WithFullPath_ReturnsNotFound(string viewName)
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, viewName, isMainPage: true);
// Assert
Assert.False(result.Success);
}
[Theory]
[MemberData(nameof(AbsoluteViewPathData))]
public void FindView_IsMainPage_WithFullPathAndCshtmlEnding_ReturnsNotFound(string viewName)
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
var context = GetActionContext(_controllerTestContext);
viewName += ".cshtml";
// Act
var result = viewEngine.FindView(context, viewName, isMainPage: true);
// Assert
Assert.False(result.Success);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void FindView_WithRelativePath_ReturnsNotFound(bool isMainPage)
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, "View.cshtml", isMainPage);
// Assert
Assert.False(result.Success);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GetView_WithViewName_ReturnsNotFound(bool isMainPage)
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
// Act
var result = viewEngine.GetView("~/Home/View1.cshtml", "View2", isMainPage);
// Assert
Assert.False(result.Success);
}
[Fact]
public void FindView_ReturnsRazorView_IfLookupWasSuccessful()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
var viewStart1 = Mock.Of<IRazorPage>();
var viewStart2 = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(GetPageFactoryResult(() => page));
pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(GetPageFactoryResult(() => viewStart2));
pageFactory
.Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
.Returns(GetPageFactoryResult(() => viewStart1));
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, "test-view", isMainPage: false);
// Assert
Assert.True(result.Success);
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
Assert.Equal("test-view", result.ViewName);
Assert.Empty(view.ViewStartPages);
}
[Fact]
public void FindView_DoesNotExpireCachedResults_IfViewStartsExpire()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
var viewStart = Mock.Of<IRazorPage>();
var cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(GetPageFactoryResult(() => page));
pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(GetPageFactoryResult(() => viewStart, new[] { changeToken }));
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act - 1
var result1 = viewEngine.FindView(context, "test-view", isMainPage: false);
// Assert - 1
Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page, view1.RazorPage);
Assert.Equal("test-view", result1.ViewName);
Assert.Empty(view1.ViewStartPages);
// Act - 2
cancellationTokenSource.Cancel();
var result2 = viewEngine.FindView(context, "test-view", isMainPage: false);
// Assert - 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page, view2.RazorPage);
pageFactory.Verify(p => p.CreateFactory("/Views/bar/test-view.cshtml"), Times.Once());
}
[Theory]
[InlineData(null, "Value cannot be null.")]
[InlineData("", "The value cannot be an empty string.")]
public void FindView_ThrowsIfViewNameIsNullOrEmpty(string partialViewName, string expectedMessage)
{
// Arrange
var viewEngine = CreateViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => viewEngine.FindView(context, partialViewName, isMainPage: false),
"viewName",
expectedMessage);
}
[Theory]
[MemberData(nameof(AbsoluteViewPathData))]
public void FindViewWithFullPath_ReturnsNotFound(string partialViewName)
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, partialViewName, isMainPage: false);
// Assert
Assert.False(result.Success);
}
[Theory]
[MemberData(nameof(AbsoluteViewPathData))]
public void FindViewWithFullPathAndCshtmlEnding_ReturnsNotFound(string partialViewName)
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
var context = GetActionContext(_controllerTestContext);
partialViewName += ".cshtml";
// Act
var result = viewEngine.FindView(context, partialViewName, isMainPage: false);
// Assert
Assert.False(result.Success);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void FindView_FailsButSearchesCorrectLocationsWithAreas(bool isMainPage)
{
// Arrange
var viewEngine = CreateViewEngine();
var context = GetActionContext(_areaTestContext);
// Act
var result = viewEngine.FindView(context, "viewName", isMainPage);
// Assert
Assert.False(result.Success);
Assert.Equal(new[]
{
"/Areas/foo/Views/bar/viewName.cshtml",
"/Areas/foo/Views/Shared/viewName.cshtml",
"/Views/Shared/viewName.cshtml",
}, result.SearchedLocations);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void FindView_FailsButSearchesCorrectLocationsWithoutAreas(bool isMainPage)
{
// Arrange
var viewEngine = CreateViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, "viewName", isMainPage);
// Assert
Assert.False(result.Success);
Assert.Equal(new[] {
"/Views/bar/viewName.cshtml",
"/Views/Shared/viewName.cshtml",
}, result.SearchedLocations);
}
[Fact]
public void FindView_IsMainPage_ReturnsRazorView_IfLookupWasSuccessful()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
var viewStart1 = Mock.Of<IRazorPage>();
var viewStart2 = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml"))
.Returns(GetPageFactoryResult(() => page));
pageFactory
.Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml"))
.Returns(GetPageFactoryResult(() => viewStart2));
pageFactory
.Setup(p => p.CreateFactory("/_ViewStart.cshtml"))
.Returns(GetPageFactoryResult(() => viewStart1));
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, "test-view", isMainPage: true);
// Assert
Assert.True(result.Success);
var view = Assert.IsType<RazorView>(result.View);
Assert.Equal("test-view", result.ViewName);
Assert.Same(page, view.RazorPage);
// ViewStartPages is not empty as it is in FindView_ReturnsRazorView_IfLookupWasSuccessful() despite
// (faked) existence of the view start files in both tests.
Assert.Equal(new[] { viewStart1, viewStart2 }, view.ViewStartPages);
}
[Fact]
public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory("fake-path1/bar/test-view.rzr"))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor(
viewLocationFormats: new[] { "fake-path1/{1}/{0}.rzr" },
areaViewLocationFormats: new[] { "fake-area-path/{2}/{1}/{0}.rzr" }));
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindView(context, "test-view", isMainPage: true);
// Assert
pageFactory.Verify();
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
}
[Fact]
public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea()
{
// Arrange
var viewName = "test-view2";
var expectedViewName = "fake-area-path/foo/bar/test-view2.rzr";
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(expectedViewName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor(
viewLocationFormats: new[] { "fake-path1/{1}/{0}.rzr" },
areaViewLocationFormats: new[] { "fake-area-path/{2}/{1}/{0}.rzr" }));
var context = GetActionContext(_areaTestContext);
// Act
var result = viewEngine.FindView(context, viewName, isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.Equal(viewName, result.ViewName);
pageFactory.Verify();
}
[Theory]
[InlineData("Test-View.cshtml")]
[InlineData("/Home/Test-View.cshtml")]
public void GetView_DoesNotUseViewLocationFormat_WithRelativePath_IfRouteDoesNotContainArea(string viewName)
{
// Arrange
var expectedViewName = "/Home/Test-View.cshtml";
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(expectedViewName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act
var result = viewEngine.GetView("/Home/Page.cshtml", viewName, isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.Equal(viewName, result.ViewName);
pageFactory.Verify();
}
[Theory]
[InlineData("Test-View.cshtml")]
[InlineData("/Home/Test-View.cshtml")]
public void GetView_DoesNotUseViewLocationFormat_WithRelativePath_IfRouteContainArea(string viewName)
{
// Arrange
var expectedViewName = "/Home/Test-View.cshtml";
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(expectedViewName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act
var result = viewEngine.GetView("/Home/Page.cshtml", viewName, isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.Equal(viewName, result.ViewName);
pageFactory.Verify();
}
[Theory]
[InlineData("/Test-View.cshtml")]
[InlineData("~/Test-View.CSHTML")]
[InlineData("/Home/Test-View.CSHTML")]
[InlineData("~/Home/Test-View.cshtml")]
[InlineData("~/SHARED/TEST-VIEW.CSHTML")]
public void GetView_UsesGivenPath_WithAppRelativePath(string viewName)
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(viewName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act
var result = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.Equal(viewName, result.ViewName);
pageFactory.Verify();
}
[Theory]
[InlineData("Test-View.cshtml")]
[InlineData("Test-View.CSHTML")]
[InlineData("PATH/TEST-VIEW.CSHTML")]
[InlineData("Path1/Path2/Test-View.cshtml")]
public void GetView_ResolvesRelativeToCurrentPage_WithRelativePath(string viewName)
{
// Arrange
var expectedViewName = $"/Home/{ viewName }";
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(expectedViewName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act
var result = viewEngine.GetView("/Home/Page.cshtml", viewName, isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.Equal(viewName, result.ViewName);
pageFactory.Verify();
}
[Theory]
[InlineData("Test-View.cshtml")]
[InlineData("Test-View.CSHTML")]
[InlineData("PATH/TEST-VIEW.CSHTML")]
[InlineData("Path1/Path2/Test-View.cshtml")]
public void GetView_ResolvesRelativeToAppRoot_WithRelativePath_IfNoPageExecuting(string viewName)
{
// Arrange
var expectedViewName = $"/{ viewName }";
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(expectedViewName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act
var result = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.Equal(viewName, result.ViewName);
pageFactory.Verify();
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void FindView_CreatesDifferentCacheEntries_ForAreaViewsAndNonAreaViews(bool isMainPage)
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var areaPage = Mock.Of<IRazorPage>();
var nonAreaPage = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"))
.Returns(GetPageFactoryResult(() => areaPage));
pageFactory
.Setup(p => p.CreateFactory("/Views/Home/Index.cshtml"))
.Returns(GetPageFactoryResult(() => nonAreaPage));
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act 1
var areaContext = GetActionContext(new Dictionary<string, object>()
{
{"area", "Admin"},
{"controller", "Home"},
});
var result1 = viewEngine.FindView(areaContext, "Index", isMainPage);
// Assert 1
Assert.NotNull(result1);
pageFactory.Verify(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"), Times.Once());
pageFactory.Verify(p => p.CreateFactory("/Views/Home/Index.cshtml"), Times.Never());
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(areaPage, view1.RazorPage);
// Act 2
var nonAreaContext = GetActionContext(new Dictionary<string, object>()
{
{"controller", "Home"},
});
var result2 = viewEngine.FindView(nonAreaContext, "Index", isMainPage);
// Assert 2
Assert.NotNull(result2);
pageFactory.Verify(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"), Times.Once());
pageFactory.Verify(p => p.CreateFactory("/Views/Home/Index.cshtml"), Times.Once());
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(nonAreaPage, view2.RazorPage);
// Act 3
// Ensure we're getting cached results.
var result3 = viewEngine.FindView(areaContext, "Index", isMainPage);
var result4 = viewEngine.FindView(nonAreaContext, "Index", isMainPage);
// Assert 4
pageFactory.Verify(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml"), Times.Once());
pageFactory.Verify(p => p.CreateFactory("/Views/Home/Index.cshtml"), Times.Once());
var view3 = Assert.IsType<RazorView>(result3.View);
Assert.Same(areaPage, view3.RazorPage);
var view4 = Assert.IsType<RazorView>(result4.View);
Assert.Same(nonAreaPage, view4.RazorPage);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void FindView_CreatesDifferentCacheEntries_ForDifferentAreas(bool isMainPage)
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var areaPage1 = Mock.Of<IRazorPage>();
var areaPage2 = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"))
.Returns(GetPageFactoryResult(() => areaPage1));
pageFactory
.Setup(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"))
.Returns(GetPageFactoryResult(() => areaPage2));
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act 1
var areaContext1 = GetActionContext(new Dictionary<string, object>()
{
{"area", "Marketing"},
{"controller", "Home"},
});
var result1 = viewEngine.FindView(areaContext1, "Index", isMainPage);
// Assert 1
Assert.NotNull(result1);
pageFactory.Verify(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"), Times.Once());
pageFactory.Verify(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"), Times.Never());
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(areaPage1, view1.RazorPage);
// Act 2
var areaContext2 = GetActionContext(new Dictionary<string, object>()
{
{"controller", "Home"},
{"area", "Sales"},
});
var result2 = viewEngine.FindView(areaContext2, "Index", isMainPage);
// Assert 2
Assert.NotNull(result2);
pageFactory.Verify(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"), Times.Once());
pageFactory.Verify(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"), Times.Once());
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(areaPage2, view2.RazorPage);
// Act 3
// Ensure we're getting cached results.
var result3 = viewEngine.FindView(areaContext1, "Index", isMainPage);
var result4 = viewEngine.FindView(areaContext2, "Index", isMainPage);
// Assert 4
pageFactory.Verify(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml"), Times.Once());
pageFactory.Verify(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml"), Times.Once());
var view3 = Assert.IsType<RazorView>(result3.View);
Assert.Same(areaPage1, view3.RazorPage);
var view4 = Assert.IsType<RazorView>(result4.View);
Assert.Same(areaPage2, view4.RazorPage);
}
[Fact]
public void FindView_UsesViewLocationExpandersToLocateViews()
{
// Arrange
var routeValues = _controllerTestContext;
var expectedSeeds = new[]
{
"/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
};
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("test-string/bar.cshtml"))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable();
var expander1Result = new[] { "some-seed" };
var expander1 = new Mock<IViewLocationExpander>();
expander1
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander1.ToString();
})
.Verifiable();
expander1
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
})
.Returns(expander1Result)
.Verifiable();
var expander2 = new Mock<IViewLocationExpander>();
expander2
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.Equal(expander1Result, seeds);
})
.Returns(new[] { "test-string/{1}.cshtml" })
.Verifiable();
var viewEngine = CreateViewEngine(
pageFactory.Object,
new[] { expander1.Object, expander2.Object });
var context = GetActionContext(routeValues);
// Act
var result = viewEngine.FindView(context, "test-view", isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.IsAssignableFrom<IView>(result.View);
pageFactory.Verify();
expander1.Verify();
expander2.Verify();
}
[Fact]
public void FindView_UsesViewLocationExpandersToLocateViews_ForAreas()
{
// Arrange
var routeValues = _areaTestContext;
var expectedSeeds = new[]
{
"/Areas/{2}/Views/{1}/{0}.cshtml",
"/Areas/{2}/Views/Shared/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
};
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("test-string/bar.cshtml"))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable();
var expander1Result = new[] { "some-seed" };
var expander1 = new Mock<IViewLocationExpander>();
expander1
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander1.ToString();
})
.Verifiable();
expander1
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
})
.Returns(expander1Result)
.Verifiable();
var expander2 = new Mock<IViewLocationExpander>();
expander2
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Callback((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.Equal(expander1Result, seeds);
})
.Returns(new[] { "test-string/{1}.cshtml" })
.Verifiable();
var viewEngine = CreateViewEngine(
pageFactory.Object,
new[] { expander1.Object, expander2.Object });
var context = GetActionContext(routeValues);
// Act
var result = viewEngine.FindView(context, "test-view", isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.IsAssignableFrom<IView>(result.View);
pageFactory.Verify();
expander1.Verify();
expander2.Verify();
}
[Fact]
public void FindView_NormalizesPaths_ReturnedByViewLocationExpanders()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory(@"Views\Home\Index.cshtml"))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable();
var expander = new Mock<IViewLocationExpander>();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns(new[] { @"Views\Home\Index.cshtml" });
var viewEngine = CreateViewEngine(
pageFactory.Object,
new[] { expander.Object });
var context = GetActionContext(new Dictionary<string, object>());
// Act
var result = viewEngine.FindView(context, "test-view", isMainPage: true);
// Assert
Assert.True(result.Success);
Assert.IsAssignableFrom<IView>(result.View);
pageFactory.Verify();
}
[Fact]
public void FindView_CachesValuesIfViewWasFound()
{
// Arrange
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(GetPageFactoryResult(factory: null))
.Verifiable();
pageFactory
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act 1
var result1 = viewEngine.FindView(context, "baz", isMainPage: true);
// Assert 1
Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page, view1.RazorPage);
pageFactory.Verify();
// Act 2
pageFactory
.Setup(p => p.CreateFactory(It.IsAny<string>()))
.Throws(new Exception("Shouldn't be called"));
var result2 = viewEngine.FindView(context, "baz", isMainPage: true);
// Assert 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page, view2.RazorPage);
pageFactory.Verify();
}
[Fact]
public void FindView_CachesValuesIfViewWasFound_ForPages()
{
// Arrange
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_pageTestContext);
// Act 1
var result1 = viewEngine.FindView(context, "baz", isMainPage: false);
// Assert 1
Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page, view1.RazorPage);
pageFactory.Verify();
// Act 2
pageFactory
.Setup(p => p.CreateFactory(It.IsAny<string>()))
.Throws(new Exception("Shouldn't be called"));
var result2 = viewEngine.FindView(context, "baz", isMainPage: false);
// Assert 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page, view2.RazorPage);
pageFactory.Verify();
}
[Fact]
public void FindView_InvokesPageFactoryIfChangeTokenExpired()
{
// Arrange
var page1 = Mock.Of<IRazorPage>();
var page2 = Mock.Of<IRazorPage>();
var sequence = new MockSequence();
var cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken }));
pageFactory
.InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(GetPageFactoryResult(() => page1))
.Verifiable();
pageFactory
.InSequence(sequence)
.Setup(p => p.CreateFactory("/Views/bar/baz.cshtml"))
.Returns(GetPageFactoryResult(() => page2));
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act 1
var result1 = viewEngine.FindView(context, "baz", isMainPage: true);
// Assert 1
Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page1, view1.RazorPage);
// Act 2
cancellationTokenSource.Cancel();
var result2 = viewEngine.FindView(context, "baz", isMainPage: true);
// Assert 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page2, view2.RazorPage);
pageFactory.Verify();
}
// This test validates an important perf scenario of RazorViewEngine not constructing
// multiple strings for views that do not exist in the file system on a per-request basis.
[Fact]
public void FindView_DoesNotInvokeViewLocationExpanders_IfChangeTokenHasNotExpired()
{
// Arrange
var pageFactory = Mock.Of<IRazorPageFactoryProvider>();
var expander = new Mock<IViewLocationExpander>();
var expandedLocations = new[]
{
"viewlocation1",
"viewlocation2",
"viewlocation3",
};
expander
.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext expanderContext) =>
{
expanderContext.Values["somekey"] = "somevalue";
})
.Verifiable();
expander
.Setup(v => v.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns(expandedLocations)
.Verifiable();
var viewEngine = CreateViewEngine(
pageFactory,
expanders: new[] { expander.Object });
var context = GetActionContext(_controllerTestContext);
// Act - 1
var result = viewEngine.FindView(context, "myview", isMainPage: true);
// Assert - 1
Assert.False(result.Success);
Assert.Equal(expandedLocations, result.SearchedLocations);
expander.Verify();
// Act - 2
result = viewEngine.FindView(context, "myview", isMainPage: true);
// Assert - 2
Assert.False(result.Success);
Assert.Equal(expandedLocations, result.SearchedLocations);
expander.Verify(
v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()),
Times.Exactly(2));
expander.Verify(
v => v.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>()),
Times.Once());
}
[Fact]
public void FindView_InvokesViewLocationExpanders_IfChangeTokenExpires()
{
// Arrange
var cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("viewlocation3"))
.Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken }));
var expander = new Mock<IViewLocationExpander>();
var expandedLocations = new[]
{
"viewlocation1",
"viewlocation2",
"viewlocation3",
};
expander
.Setup(v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext expanderContext) =>
{
expanderContext.Values["somekey"] = "somevalue";
})
.Verifiable();
expander
.Setup(v => v.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns(expandedLocations)
.Verifiable();
var viewEngine = CreateViewEngine(
pageFactory.Object,
expanders: new[] { expander.Object });
var context = GetActionContext(_controllerTestContext);
// Act - 1
var result = viewEngine.FindView(context, "MyView", isMainPage: true);
// Assert - 1
Assert.False(result.Success);
Assert.Equal(expandedLocations, result.SearchedLocations);
expander.Verify();
// Act - 2
pageFactory
.Setup(p => p.CreateFactory("viewlocation3"))
.Returns(GetPageFactoryResult(() => page));
cancellationTokenSource.Cancel();
result = viewEngine.FindView(context, "MyView", isMainPage: true);
// Assert - 2
Assert.True(result.Success);
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
expander.Verify(
v => v.PopulateValues(It.IsAny<ViewLocationExpanderContext>()),
Times.Exactly(2));
expander.Verify(
v => v.ExpandViewLocations(It.IsAny<ViewLocationExpanderContext>(), It.IsAny<IEnumerable<string>>()),
Times.Exactly(2));
}
[Theory]
[MemberData(nameof(AbsoluteViewPathData))]
public void FindPage_WithFullPath_ReturnsNotFound(string viewName)
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindPage(context, viewName);
// Assert
Assert.Null(result.Page);
}
[Theory]
[MemberData(nameof(AbsoluteViewPathData))]
public void FindPage_WithFullPathAndCshtmlEnding_ReturnsNotFound(string viewName)
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
var context = GetActionContext(_controllerTestContext);
viewName += ".cshtml";
// Act
var result = viewEngine.FindPage(context, viewName);
// Assert
Assert.Null(result.Page);
}
[Fact]
public void FindPage_WithRelativePath_ReturnsNotFound()
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindPage(context, "View.cshtml");
// Assert
Assert.Null(result.Page);
}
[Fact]
public void GetPage_WithViewName_ReturnsNotFound()
{
// Arrange
var viewEngine = CreateSuccessfulViewEngine();
// Act
var result = viewEngine.GetPage("~/Home/View1.cshtml", "View2");
// Assert
Assert.Null(result.Page);
}
[Theory]
[InlineData(null, "Value cannot be null.")]
[InlineData("", "The value cannot be an empty string.")]
public void FindPage_ThrowsIfNameIsNullOrEmpty(string pageName, string expectedMessage)
{
// Arrange
var viewEngine = CreateViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => viewEngine.FindPage(context, pageName),
"pageName",
expectedMessage);
}
[Fact]
public void FindPage_UsesViewLocationExpander_ToExpandPaths()
{
// Arrange
var routeValues = _controllerTestContext;
var expectedSeeds = new[]
{
"/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
};
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("expanded-path/bar-layout"))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var expander = new Mock<IViewLocationExpander>();
expander
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander.ToString();
})
.Verifiable();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
Assert.Equal(expander.ToString(), c.Values["expander-key"]);
return new[] { "expanded-path/bar-{0}" };
})
.Verifiable();
var viewEngine = CreateViewEngine(
pageFactory.Object,
new[] { expander.Object });
var context = GetActionContext(routeValues);
// Act
var result = viewEngine.FindPage(context, "layout");
// Assert
Assert.Equal("layout", result.Name);
Assert.Same(page, result.Page);
Assert.Null(result.SearchedLocations);
pageFactory.Verify();
expander.Verify();
}
[Fact]
public void FindPage_UsesViewLocationExpander_ToExpandPaths_ForAreas()
{
// Arrange
var routeValues = _areaTestContext;
var expectedSeeds = new[]
{
"/Areas/{2}/Views/{1}/{0}.cshtml",
"/Areas/{2}/Views/Shared/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
};
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("expanded-path/bar-layout"))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var expander = new Mock<IViewLocationExpander>();
expander
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.NotNull(c.ActionContext);
c.Values["expander-key"] = expander.ToString();
})
.Verifiable();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns((ViewLocationExpanderContext c, IEnumerable<string> seeds) =>
{
Assert.NotNull(c.ActionContext);
Assert.Equal(expectedSeeds, seeds);
Assert.Equal(expander.ToString(), c.Values["expander-key"]);
return new[] { "expanded-path/bar-{0}" };
})
.Verifiable();
var viewEngine = CreateViewEngine(
pageFactory.Object,
new[] { expander.Object });
var context = GetActionContext(routeValues);
// Act
var result = viewEngine.FindPage(context, "layout");
// Assert
Assert.Equal("layout", result.Name);
Assert.Same(page, result.Page);
Assert.Null(result.SearchedLocations);
pageFactory.Verify();
expander.Verify();
}
[Fact]
public void FindPage_ReturnsSearchedLocationsIfPageCannotBeFound()
{
// Arrange
var expected = new[]
{
"/Views/bar/layout.cshtml",
"/Views/Shared/layout.cshtml",
};
var viewEngine = CreateViewEngine();
var context = GetActionContext(_controllerTestContext);
// Act
var result = viewEngine.FindPage(context, "layout");
// Assert
Assert.Equal("layout", result.Name);
Assert.Null(result.Page);
Assert.Equal(expected, result.SearchedLocations);
}
[Fact]
public void FindPage_SelectsActionCaseInsensitively()
{
// The ActionDescriptor contains "Foo" and the RouteData contains "foo"
// which matches the case of the constructor thus searching in the appropriate location.
// Arrange
var routeValues = new Dictionary<string, object>
{
{ "controller", "foo" }
};
var page = new Mock<IRazorPage>(MockBehavior.Strict);
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("/Views/Foo/details.cshtml"))
.Returns(GetPageFactoryResult(() => page.Object))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var routesInActionDescriptor = new Dictionary<string, string>()
{
{ "controller", "Foo" }
};
var context = GetActionContextWithActionDescriptor(
routeValues,
routesInActionDescriptor);
// Act
var result = viewEngine.FindPage(context, "details");
// Assert
Assert.Equal("details", result.Name);
Assert.Same(page.Object, result.Page);
Assert.Null(result.SearchedLocations);
pageFactory.Verify();
}
[Fact]
public void FindPage_LooksForPages_UsingActionDescriptor_Controller()
{
// Arrange
var expected = new[]
{
"/Views/bar/foo.cshtml",
"/Views/Shared/foo.cshtml",
};
var routeValues = new Dictionary<string, object>
{
{ "controller", "Bar" }
};
var routesInActionDescriptor = new Dictionary<string, string>()
{
{ "controller", "bar" }
};
var viewEngine = CreateViewEngine();
var context = GetActionContextWithActionDescriptor(
routeValues,
routesInActionDescriptor);
// Act
var result = viewEngine.FindPage(context, "foo");
// Assert
Assert.Equal("foo", result.Name);
Assert.Null(result.Page);
Assert.Equal(expected, result.SearchedLocations);
}
[Fact]
public void FindPage_LooksForPages_UsingActionDescriptor_Areas()
{
// Arrange
var expected = new[]
{
"/Areas/world/Views/bar/foo.cshtml",
"/Areas/world/Views/Shared/foo.cshtml",
"/Views/Shared/foo.cshtml"
};
var routeValues = new Dictionary<string, object>
{
{ "controller", "Bar" },
{ "area", "World" }
};
var routesInActionDescriptor = new Dictionary<string, string>()
{
{ "controller", "bar" },
{ "area", "world" }
};
var viewEngine = CreateViewEngine();
var context = GetActionContextWithActionDescriptor(
routeValues,
routesInActionDescriptor);
// Act
var result = viewEngine.FindPage(context, "foo");
// Assert
Assert.Equal("foo", result.Name);
Assert.Null(result.Page);
Assert.Equal(expected, result.SearchedLocations);
}
[Fact]
public void FindPage_LooksForPages_UsesRouteValuesAsFallback()
{
// Arrange
var expected = new[]
{
"/Views/foo/bar.cshtml",
"/Views/Shared/bar.cshtml",
};
var routeValues = new Dictionary<string, object>()
{
{ "controller", "foo" }
};
var viewEngine = CreateViewEngine();
var context = GetActionContextWithActionDescriptor(
routeValues,
new Dictionary<string, string>());
// Act
var result = viewEngine.FindPage(context, "bar");
// Assert
Assert.Equal("bar", result.Name);
Assert.Null(result.Page);
Assert.Equal(expected, result.SearchedLocations);
}
[Theory]
[InlineData("/Test-View.cshtml")]
[InlineData("~/Test-View.CSHTML")]
[InlineData("/Home/Test-View.CSHTML")]
[InlineData("~/Home/Test-View.cshtml")]
[InlineData("~/SHARED/TEST-VIEW.CSHTML")]
public void GetPage_UsesGivenPath_WithAppRelativePath(string pageName)
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(pageName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act
var result = viewEngine.GetPage("~/Another/Place.cshtml", pagePath: pageName);
// Assert
Assert.Same(page, result.Page);
Assert.Equal(pageName, result.Name);
pageFactory.Verify();
}
[Theory]
[InlineData("Test-View.cshtml")]
[InlineData("Test-View.CSHTML")]
[InlineData("PATH/TEST-VIEW.CSHTML")]
[InlineData("Path1/Path2/Test-View.cshtml")]
public void GetPage_ResolvesRelativeToCurrentPage_WithRelativePath(string pageName)
{
// Arrange
var expectedPageName = $"/Home/{ pageName }";
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(expectedPageName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act
var result = viewEngine.GetPage("/Home/Page.cshtml", pageName);
// Assert
Assert.Same(page, result.Page);
Assert.Equal(pageName, result.Name);
pageFactory.Verify();
}
[Theory]
[InlineData("Test-View.cshtml")]
[InlineData("Test-View.CSHTML")]
[InlineData("PATH/TEST-VIEW.CSHTML")]
[InlineData("Path1/Path2/Test-View.cshtml")]
public void GetPage_ResolvesRelativeToAppRoot_WithRelativePath_IfNoPageExecuting(string pageName)
{
// Arrange
var expectedPageName = $"/{ pageName }";
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory(expectedPageName))
.Returns(GetPageFactoryResult(() => page))
.Verifiable();
var viewEngine = new TestableRazorViewEngine(
pageFactory.Object,
GetOptionsAccessor());
// Act
var result = viewEngine.GetPage(executingFilePath: null, pagePath: pageName);
// Assert
Assert.Same(page, result.Page);
Assert.Equal(pageName, result.Name);
pageFactory.Verify();
}
[Theory]
[InlineData(null, null)]
[InlineData(null, "")]
[InlineData(null, "Page")]
[InlineData(null, "Folder/Page")]
[InlineData(null, "Folder1/Folder2/Page")]
[InlineData("/Home/Index.cshtml", null)]
[InlineData("/Home/Index.cshtml", "")]
[InlineData("/Home/Index.cshtml", "Page")]
[InlineData("/Home/Index.cshtml", "Folder/Page")]
[InlineData("/Home/Index.cshtml", "Folder1/Folder2/Page")]
public void GetAbsolutePath_ReturnsPagePathUnchanged_IfNotAPath(string executingFilePath, string pagePath)
{
// Arrange
var viewEngine = CreateViewEngine();
// Act
var result = viewEngine.GetAbsolutePath(executingFilePath, pagePath);
// Assert
Assert.Same(pagePath, result);
}
[Theory]
[InlineData("/Views/Home/Index.cshtml", "../Shared/_Partial.cshtml")]
[InlineData("/Views/Home/Index.cshtml", "..\\Shared\\_Partial.cshtml")]
[InlineData("/Areas/MyArea/Views/Home/Index.cshtml", "../../../../Views/Shared/_Partial.cshtml")]
[InlineData("/Views/Accounts/Users.cshtml", "../Test/../Shared/_Partial.cshtml")]
[InlineData("Views/Accounts/Users.cshtml", "./../Shared/./_Partial.cshtml")]
public void GetAbsolutePath_ResolvesPathTraversals(string executingFilePath, string pagePath)
{
// Arrange
var viewEngine = CreateViewEngine();
// Act
var result = viewEngine.GetAbsolutePath(executingFilePath, pagePath);
// Assert
Assert.Equal("/Views/Shared/_Partial.cshtml", result);
}
[Theory]
[InlineData("../Shared/_Layout.cshtml")]
[InlineData("Folder1/../Folder2/../../File.cshtml")]
public void GetAbsolutePath_DoesNotResolvePathIfTraversalsEscapeTheRoot(string pagePath)
{
// Arrange
var expected = '/' + pagePath;
var viewEngine = CreateViewEngine();
// Act
var result = viewEngine.GetAbsolutePath("/Index.cshtml", pagePath);
// Assert
Assert.Equal(expected, result);
}
[Theory]
[InlineData(null, "/Page")]
[InlineData(null, "~/Folder/Page.cshtml")]
[InlineData(null, "/Folder1/Folder2/Page.rzr")]
[InlineData("/Home/Index.cshtml", "~/Page")]
[InlineData("/Home/Index.cshtml", "/Folder/Page.cshtml")]
[InlineData("/Home/Index.cshtml", "~/Folder1/Folder2/Page.rzr")]
public void GetAbsolutePath_ReturnsPagePathUnchanged_IfAppRelative(string executingFilePath, string pagePath)
{
// Arrange
var viewEngine = CreateViewEngine();
// Act
var result = viewEngine.GetAbsolutePath(executingFilePath, pagePath);
// Assert
Assert.Same(pagePath, result);
}
[Theory]
[InlineData("Page.cshtml")]
[InlineData("Folder/Page.cshtml")]
[InlineData("../../Folder1/Folder2/Page.cshtml")]
public void GetAbsolutePath_ResolvesRelativeToExecutingPage(string pagePath)
{
// Arrange
var expectedPagePath = "/Home/" + pagePath;
var viewEngine = CreateViewEngine();
// Act
var result = viewEngine.GetAbsolutePath("/Home/Page.cshtml", pagePath);
// Assert
Assert.Equal(expectedPagePath, result);
}
[Theory]
[InlineData("Page.cshtml")]
[InlineData("Folder/Page.cshtml")]
[InlineData("../../Folder1/Folder2/Page.cshtml")]
public void GetAbsolutePath_ResolvesRelativeToAppRoot_IfNoPageExecuting(string pagePath)
{
// Arrange
var expectedPagePath = "/" + pagePath;
var viewEngine = CreateViewEngine();
// Act
var result = viewEngine.GetAbsolutePath(executingFilePath: null, pagePath: pagePath);
// Assert
Assert.Equal(expectedPagePath, result);
}
[Fact]
public void GetNormalizedRouteValue_ReturnsValueFromRouteValues()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor();
actionDescriptor.RouteValues.Add(key, "Route-Value");
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("Route-Value", result);
}
[Fact]
[ReplaceCulture("de-CH", "de-CH")]
public void GetNormalizedRouteValue_UsesInvariantCulture()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor();
actionDescriptor.RouteValues.Add(key, "Route-Value");
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = new DateTimeOffset(2018, 10, 31, 7, 37, 38, TimeSpan.FromHours(-7));
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("10/31/2018 07:37:38 -07:00", result);
}
[Fact]
public void GetNormalizedRouteValue_ReturnsRouteValue_IfValueDoesNotMatch()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor();
actionDescriptor.RouteValues.Add(key, "different-value");
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("route-value", result);
}
[Fact]
public void GetNormalizedRouteValue_ReturnsNonNormalizedValue_IfActionRouteValueIsNull()
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor();
actionDescriptor.RouteValues.Add(key, null);
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = "route-value";
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("route-value", result);
}
[Fact]
public void GetNormalizedRouteValue_ConvertsRouteValueToString()
{
using (new CultureReplacer())
{
// Arrange
var key = "some-key";
var actionDescriptor = new ActionDescriptor
{
AttributeRouteInfo = new AttributeRouteInfo(),
};
var actionContext = new ActionContext
{
ActionDescriptor = actionDescriptor,
RouteData = new RouteData()
};
actionContext.RouteData.Values[key] = 43;
// Act
var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key);
// Assert
Assert.Equal("43", result);
}
}
[Fact]
public void GetViewLocationFormats_ForControllerWithoutArea_ReturnsDefaultSet()
{
// Arrange
var expected = new string[] { "expected", };
var viewEngine = new TestableRazorViewEngine(
Mock.Of<IRazorPageFactoryProvider>(),
GetOptionsAccessor(viewLocationFormats: expected));
var context = new ViewLocationExpanderContext(
new ActionContext(),
"Index.cshtml",
controllerName: "Home",
areaName: null,
pageName: "ignored",
isMainPage: true);
// Act
var actual = viewEngine.GetViewLocationFormats(context);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void GetViewLocationFormats_ForControllerWithArea_ReturnsAreaSet()
{
// Arrange
var expected = new string[] { "expected", };
var viewEngine = new TestableRazorViewEngine(
Mock.Of<IRazorPageFactoryProvider>(),
GetOptionsAccessor(areaViewLocationFormats: expected));
var context = new ViewLocationExpanderContext(
new ActionContext(),
"Index.cshtml",
controllerName: "Home",
areaName: "Admin",
pageName: "ignored",
isMainPage: true);
// Act
var actual = viewEngine.GetViewLocationFormats(context);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void GetViewLocationFormats_ForPage_ReturnsPageSet()
{
// Arrange
var expected = new string[] { "expected", };
var viewEngine = new TestableRazorViewEngine(
Mock.Of<IRazorPageFactoryProvider>(),
GetOptionsAccessor(pageViewLocationFormats: expected));
var context = new ViewLocationExpanderContext(
new ActionContext(),
"Index.cshtml",
controllerName: null,
areaName: null,
pageName: "/Some/Page",
isMainPage: true);
// Act
var actual = viewEngine.GetViewLocationFormats(context);
// Assert
Assert.Equal(expected, actual);
}
// This isn't a real case we expect to hit in an app, just making sure we have a reasonable default
// for a weird configuration. In this case we preserve what we did in 1.0.0.
[Fact]
public void GetViewLocationFormats_NoRouteValues_ReturnsDefaultSet()
{
// Arrange
var expected = new string[] { "expected", };
var viewEngine = new TestableRazorViewEngine(
Mock.Of<IRazorPageFactoryProvider>(),
GetOptionsAccessor(viewLocationFormats: expected));
var context = new ViewLocationExpanderContext(
new ActionContext(),
"Index.cshtml",
controllerName: null,
areaName: null,
pageName: null,
isMainPage: true);
// Act
var actual = viewEngine.GetViewLocationFormats(context);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void ViewEngine_DoesNotSetPageValue_IfItIsNotSpecifiedInRouteValues()
{
// Arrange
var routeValues = new Dictionary<string, object>
{
{ "controller", "MyController" },
{ "action", "MyAction" }
};
var expected = new[] { "some-seed" };
var expander = new Mock<IViewLocationExpander>();
expander
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.Equal("MyController", c.ControllerName);
Assert.Null(c.PageName);
})
.Verifiable();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns(expected);
var viewEngine = CreateViewEngine(expanders: new[] { expander.Object });
var context = GetActionContext(routeValues);
// Act
viewEngine.FindView(context, viewName: "Test-view", isMainPage: true);
// Assert
expander.Verify();
}
[Fact]
public void ViewEngine_SetsPageValue_IfItIsSpecifiedInRouteValues()
{
// Arrange
var routeValues = new Dictionary<string, object>
{
{ "page", "MyPage" },
};
var expected = new[] { "some-seed" };
var expander = new Mock<IViewLocationExpander>();
expander
.Setup(e => e.PopulateValues(It.IsAny<ViewLocationExpanderContext>()))
.Callback((ViewLocationExpanderContext c) =>
{
Assert.Equal("MyPage", c.PageName);
})
.Verifiable();
expander
.Setup(e => e.ExpandViewLocations(
It.IsAny<ViewLocationExpanderContext>(),
It.IsAny<IEnumerable<string>>()))
.Returns(expected);
var viewEngine = CreateViewEngine(expanders: new[] { expander.Object });
var context = GetActionContext(routeValues);
context.ActionDescriptor.RouteValues["page"] = "MyPage";
// Act
viewEngine.FindView(context, viewName: "MyView", isMainPage: true);
// Assert
expander.Verify();
}
[Fact]
public void FindView_ResolvesDirectoryTraversalsPriorToInvokingPageFactory()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory.Setup(p => p.CreateFactory("/Views/Shared/_Partial.cshtml"))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(new Dictionary<string, object>());
// Act
var result = viewEngine.FindView(context, "../Shared/_Partial", isMainPage: false);
// Assert
pageFactory.Verify();
}
[Fact]
public void FindPage_ResolvesDirectoryTraversalsPriorToInvokingPageFactory()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory.Setup(p => p.CreateFactory("/Views/Shared/_Partial.cshtml"))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(new Dictionary<string, object>());
// Act
var result = viewEngine.FindPage(context, "../Shared/_Partial");
// Assert
pageFactory.Verify();
}
// Tests to verify fix for https://github.com/aspnet/Mvc/issues/6672
// Without normalizing the path, the view engine would have attempted to lookup "/Views//MyView.cshtml"
// which works for PhysicalFileProvider but fails for exact lookups performed during precompilation.
// We normalize it to "/Views/MyView.cshtml" to avoid this discrepancy.
[Fact]
public void FindView_ResolvesNormalizesSlashesPriorToInvokingPageFactory()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory.Setup(p => p.CreateFactory("/Views/MyView.cshtml"))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(new Dictionary<string, object>());
// Act
var result = viewEngine.FindView(context, "MyView", isMainPage: true);
// Assert
pageFactory.Verify();
}
[Fact]
public void FindPage_ResolvesNormalizesSlashesPriorToInvokingPageFactory()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory.Setup(p => p.CreateFactory("/Views/MyPage.cshtml"))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(new Dictionary<string, object>());
// Act
var result = viewEngine.FindPage(context, "MyPage");
// Assert
pageFactory.Verify();
}
[Fact]
public void FindView_Succeeds_AfterClearCache()
{
// Arrange
var pageFactory = new Mock<IRazorPageFactoryProvider>();
var page = Mock.Of<IRazorPage>();
var hotReloadedPage = Mock.Of<IRazorPage>();
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/Index.cshtml"))
.Returns(GetPageFactoryResult(() => page));
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_controllerTestContext);
// Act - 1
// First verify results are typically cached
for (var i = 0; i < 3; i++)
{
var result = viewEngine.FindView(context, "Index", isMainPage: false);
// Assert - 1
Assert.True(result.Success);
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(page, view.RazorPage);
pageFactory.Verify(p => p.CreateFactory("/Views/bar/Index.cshtml"), Times.Once());
}
// Now verify that the page factory is re-invoked after cache clear
viewEngine.ClearCache();
pageFactory
.Setup(p => p.CreateFactory("/Views/bar/Index.cshtml"))
.Returns(GetPageFactoryResult(() => hotReloadedPage));
{
// Act - 2
var result = viewEngine.FindView(context, "Index", isMainPage: false);
// Assert - 2
Assert.True(result.Success);
var view = Assert.IsType<RazorView>(result.View);
Assert.Same(hotReloadedPage, view.RazorPage);
pageFactory.Verify(p => p.CreateFactory("/Views/bar/Index.cshtml"), Times.Exactly(2));
}
}
// Return RazorViewEngine with a page factory provider that is always successful.
private RazorViewEngine CreateSuccessfulViewEngine()
{
var pageFactory = new Mock<IRazorPageFactoryProvider>(MockBehavior.Strict);
pageFactory
.Setup(f => f.CreateFactory(It.IsAny<string>()))
.Returns(GetPageFactoryResult(() => Mock.Of<IRazorPage>()));
return CreateViewEngine(pageFactory.Object);
}
private static RazorPageFactoryResult GetPageFactoryResult(
Func<IRazorPage> factory,
IList<IChangeToken> changeTokens = null,
string path = "/Views/Home/Index.cshtml")
{
var descriptor = new CompiledViewDescriptor
{
ExpirationTokens = changeTokens ?? Array.Empty<IChangeToken>(),
RelativePath = path,
};
return new RazorPageFactoryResult(descriptor, factory);
}
private TestableRazorViewEngine CreateViewEngine(
IRazorPageFactoryProvider pageFactory = null,
IEnumerable<IViewLocationExpander> expanders = null)
{
pageFactory = pageFactory ?? Mock.Of<IRazorPageFactoryProvider>();
return new TestableRazorViewEngine(pageFactory, GetOptionsAccessor(expanders));
}
private static IOptions<RazorViewEngineOptions> GetOptionsAccessor(
IEnumerable<IViewLocationExpander> expanders = null,
IEnumerable<string> viewLocationFormats = null,
IEnumerable<string> areaViewLocationFormats = null,
IEnumerable<string> pageViewLocationFormats = null)
{
var optionsSetup = new RazorViewEngineOptionsSetup();
var options = new RazorViewEngineOptions();
optionsSetup.Configure(options);
options.PageViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
options.PageViewLocationFormats.Add("/Pages/Shared/{0}.cshtml");
if (expanders != null)
{
foreach (var expander in expanders)
{
options.ViewLocationExpanders.Add(expander);
}
}
if (viewLocationFormats != null)
{
options.ViewLocationFormats.Clear();
foreach (var location in viewLocationFormats)
{
options.ViewLocationFormats.Add(location);
}
}
if (areaViewLocationFormats != null)
{
options.AreaViewLocationFormats.Clear();
foreach (var location in areaViewLocationFormats)
{
options.AreaViewLocationFormats.Add(location);
}
}
if (pageViewLocationFormats != null)
{
options.PageViewLocationFormats.Clear();
foreach (var location in pageViewLocationFormats)
{
options.PageViewLocationFormats.Add(location);
}
}
var optionsAccessor = new Mock<IOptions<RazorViewEngineOptions>>();
optionsAccessor
.SetupGet(v => v.Value)
.Returns(options);
return optionsAccessor.Object;
}
private static ActionContext GetActionContext(IDictionary<string, object> routeValues)
{
var httpContext = new DefaultHttpContext();
var routeData = new RouteData();
foreach (var kvp in routeValues)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
var actionDescriptor = new ActionDescriptor();
return new ActionContext(httpContext, routeData, actionDescriptor);
}
private static ActionContext GetActionContextWithActionDescriptor(
IDictionary<string, object> routeValues,
IDictionary<string, string> actionRouteValues)
{
var httpContext = new DefaultHttpContext();
var routeData = new RouteData();
foreach (var kvp in routeValues)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
var actionDescriptor = new ActionDescriptor();
foreach (var kvp in actionRouteValues)
{
actionDescriptor.RouteValues.Add(kvp.Key, kvp.Value);
}
return new ActionContext(httpContext, routeData, actionDescriptor);
}
private class TestableRazorViewEngine : RazorViewEngine
{
public TestableRazorViewEngine(
IRazorPageFactoryProvider pageFactory,
IOptions<RazorViewEngineOptions> optionsAccessor)
: base(pageFactory, Mock.Of<IRazorPageActivator>(), new HtmlTestEncoder(), optionsAccessor, NullLoggerFactory.Instance, new DiagnosticListener("Microsoft.AspNetCore.Mvc.Razor"))
{
}
public IMemoryCache ViewLookupCachePublic => ViewLookupCache;
}
}
|