// 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.Collections.Generic; using System.Threading; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.ViewEngines; using Microsoft.AspNet.Routing; using Microsoft.AspNet.Testing; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.OptionsModel; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.WebEncoders.Testing; using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.Razor.Test { public class RazorViewEngineTest { private static readonly Dictionary _areaTestContext = new Dictionary() { {"area", "foo"}, {"controller", "bar"}, }; private static readonly Dictionary _controllerTestContext = new Dictionary() { {"controller", "bar"}, }; public static IEnumerable InvalidViewNameValues { get { yield return new[] { "~/foo/bar" }; yield return new[] { "/foo/bar" }; yield return new[] { "~/foo/bar.txt" }; yield return new[] { "/foo/bar.txt" }; } } public static IEnumerable ViewLocationExpanderTestData { get { yield return new object[] { _controllerTestContext, new[] { "/Views/{1}/{0}.cshtml", "/Views/Shared/{0}.cshtml" } }; yield return new object[] { _areaTestContext, new[] { "/Areas/{2}/Views/{1}/{0}.cshtml", "/Areas/{2}/Views/Shared/{0}.cshtml", "/Views/Shared/{0}.cshtml" } }; } } [Theory] [InlineData(null)] [InlineData("")] public void FindView_ThrowsIfViewNameIsNullOrEmpty(string viewName) { // Arrange var viewEngine = CreateViewEngine(); var context = GetActionContext(_controllerTestContext); // Act & Assert ExceptionAssert.ThrowsArgumentNullOrEmpty(() => viewEngine.FindView(context, viewName), "viewName"); } [Theory] [MemberData(nameof(InvalidViewNameValues))] public void FindView_WithFullPathReturnsNotFound_WhenPathDoesNotMatchExtension(string viewName) { // Arrange var viewEngine = CreateViewEngine(); var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindView(context, viewName); // Assert Assert.False(result.Success); } [Theory] [MemberData(nameof(InvalidViewNameValues))] public void FindViewFullPathSucceedsWithCshtmlEnding(string viewName) { // Arrange var viewEngine = CreateViewEngine(); // Append .cshtml so the viewname is no longer invalid viewName += ".cshtml"; var context = GetActionContext(_controllerTestContext); // Act & Assert // If this throws then our test case fails var result = viewEngine.FindPartialView(context, viewName); Assert.False(result.Success); } [Fact] public void FindPartialView_ReturnsRazorView_IfLookupWasSuccessful() { // Arrange var pageFactory = new Mock(); var page = Mock.Of(); var viewStart1 = Mock.Of(); var viewStart2 = Mock.Of(); pageFactory .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); pageFactory .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => viewStart2, new IChangeToken[0])); pageFactory .Setup(p => p.CreateFactory("/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => viewStart1, new IChangeToken[0])); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindPartialView(context, "test-view"); // Assert Assert.True(result.Success); var view = Assert.IsType(result.View); Assert.Same(page, view.RazorPage); Assert.Equal("test-view", result.ViewName); Assert.Empty(view.ViewStartPages); } [Fact] public void FindPartialView_DoesNotExpireCachedResults_IfViewStartsExpire() { // Arrange var pageFactory = new Mock(); var page = Mock.Of(); var viewStart = Mock.Of(); var cancellationTokenSource = new CancellationTokenSource(); var changeToken = new CancellationChangeToken(cancellationTokenSource.Token); pageFactory .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); pageFactory .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => viewStart, new[] { changeToken })); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); // Act - 1 var result1 = viewEngine.FindPartialView(context, "test-view"); // Assert - 1 Assert.True(result1.Success); var view1 = Assert.IsType(result1.View); Assert.Same(page, view1.RazorPage); Assert.Equal("test-view", result1.ViewName); Assert.Empty(view1.ViewStartPages); // Act - 2 cancellationTokenSource.Cancel(); var result2 = viewEngine.FindPartialView(context, "test-view"); // Assert - 2 Assert.True(result2.Success); var view2 = Assert.IsType(result2.View); Assert.Same(page, view2.RazorPage); pageFactory.Verify(p => p.CreateFactory("/Views/bar/test-view.cshtml"), Times.Once()); } [Theory] [InlineData(null)] [InlineData("")] public void FindPartialView_ThrowsIfViewNameIsNullOrEmpty(string partialViewName) { // Arrange var viewEngine = CreateViewEngine(); var context = GetActionContext(_controllerTestContext); // Act & Assert ExceptionAssert.ThrowsArgumentNullOrEmpty( () => viewEngine.FindPartialView(context, partialViewName), "partialViewName"); } [Theory] [MemberData(nameof(InvalidViewNameValues))] public void FindPartialView_WithFullPathReturnsNotFound_WhenPathDoesNotMatchExtension(string partialViewName) { // Arrange var viewEngine = CreateViewEngine(); var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindPartialView(context, partialViewName); // Assert Assert.False(result.Success); } [Theory] [MemberData(nameof(InvalidViewNameValues))] public void FindPartialViewFullPathSucceedsWithCshtmlEnding(string partialViewName) { // Arrange var viewEngine = CreateViewEngine(); // Append .cshtml so the viewname is no longer invalid partialViewName += ".cshtml"; var context = GetActionContext(_controllerTestContext); // Act & Assert // If this throws then our test case fails var result = viewEngine.FindPartialView(context, partialViewName); Assert.False(result.Success); } [Fact] public void FindPartialViewFailureSearchesCorrectLocationsWithAreas() { // Arrange var searchedLocations = new List(); var viewEngine = CreateViewEngine(); var context = GetActionContext(_areaTestContext); // Act var result = viewEngine.FindPartialView(context, "partial"); // Assert Assert.False(result.Success); Assert.Equal(new[] { "/Areas/foo/Views/bar/partial.cshtml", "/Areas/foo/Views/Shared/partial.cshtml", "/Views/Shared/partial.cshtml", }, result.SearchedLocations); } [Fact] public void FindPartialViewFailureSearchesCorrectLocationsWithoutAreas() { // Arrange var viewEngine = CreateViewEngine(); var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindPartialView(context, "partialNoArea"); // Assert Assert.False(result.Success); Assert.Equal(new[] { "/Views/bar/partialNoArea.cshtml", "/Views/Shared/partialNoArea.cshtml", }, result.SearchedLocations); } [Fact] public void FindViewFailureSearchesCorrectLocationsWithAreas() { // Arrange var viewEngine = CreateViewEngine(); var context = GetActionContext(_areaTestContext); // Act var result = viewEngine.FindView(context, "full"); // Assert Assert.False(result.Success); Assert.Equal(new[] { "/Areas/foo/Views/bar/full.cshtml", "/Areas/foo/Views/Shared/full.cshtml", "/Views/Shared/full.cshtml", }, result.SearchedLocations); } [Fact] public void FindViewFailureSearchesCorrectLocationsWithoutAreas() { // Arrange var viewEngine = CreateViewEngine(); var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindView(context, "fullNoArea"); // Assert Assert.False(result.Success); Assert.Equal(new[] { "/Views/bar/fullNoArea.cshtml", "/Views/Shared/fullNoArea.cshtml", }, result.SearchedLocations); } [Fact] public void FindView_ReturnsRazorView_IfLookupWasSuccessful() { // Arrange var pageFactory = new Mock(); var page = Mock.Of(); var viewStart1 = Mock.Of(); var viewStart2 = Mock.Of(); pageFactory .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); pageFactory .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => viewStart2, new IChangeToken[0])); pageFactory .Setup(p => p.CreateFactory("/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => viewStart1, new IChangeToken[0])); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindView(context, "test-view"); // Assert Assert.True(result.Success); var view = Assert.IsType(result.View); Assert.Equal("test-view", result.ViewName); Assert.Same(page, view.RazorPage); Assert.False(view.IsPartial); Assert.Equal(new[] { viewStart1, viewStart2 }, view.ViewStartPages); } [Fact] public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea() { // Arrange var pageFactory = new Mock(); var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory("fake-path1/bar/test-view.rzr")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, GetOptionsAccessor()); viewEngine.SetLocationFormats( new[] { "fake-path1/{1}/{0}.rzr" }, new[] { "fake-area-path/{2}/{1}/{0}.rzr" }); var context = GetActionContext(_controllerTestContext); // Act var result = viewEngine.FindView(context, "test-view"); // Assert pageFactory.Verify(); var view = Assert.IsType(result.View); Assert.Same(page, view.RazorPage); } [Fact] public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea() { // Arrange var pageFactory = new Mock(); var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory("fake-area-path/foo/bar/test-view2.rzr")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, GetOptionsAccessor()); viewEngine.SetLocationFormats( new[] { "fake-path1/{1}/{0}.rzr" }, new[] { "fake-area-path/{2}/{1}/{0}.rzr" }); var context = GetActionContext(_areaTestContext); // Act var result = viewEngine.FindView(context, "test-view2"); // Assert pageFactory.Verify(); var view = Assert.IsType(result.View); Assert.Same(page, view.RazorPage); } [Theory] [MemberData(nameof(ViewLocationExpanderTestData))] public void FindView_UsesViewLocationExpandersToLocateViews( IDictionary routeValues, IEnumerable expectedSeeds) { // Arrange var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("test-string/bar.cshtml")) .Returns(new RazorPageFactoryResult(() => Mock.Of(), new IChangeToken[0])) .Verifiable(); var expander1Result = new[] { "some-seed" }; var expander1 = new Mock(); expander1 .Setup(e => e.PopulateValues(It.IsAny())) .Callback((ViewLocationExpanderContext c) => { Assert.NotNull(c.ActionContext); c.Values["expander-key"] = expander1.ToString(); }) .Verifiable(); expander1 .Setup(e => e.ExpandViewLocations( It.IsAny(), It.IsAny>())) .Callback((ViewLocationExpanderContext c, IEnumerable seeds) => { Assert.NotNull(c.ActionContext); Assert.Equal(expectedSeeds, seeds); }) .Returns(expander1Result) .Verifiable(); var expander2 = new Mock(); expander2 .Setup(e => e.ExpandViewLocations( It.IsAny(), It.IsAny>())) .Callback((ViewLocationExpanderContext c, IEnumerable 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"); // Assert Assert.True(result.Success); Assert.IsAssignableFrom(result.View); pageFactory.Verify(); expander1.Verify(); expander2.Verify(); } [Fact] public void FindView_CachesValuesIfViewWasFound() { // Arrange var page = Mock.Of(); var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Returns(new RazorPageFactoryResult(new IChangeToken[0])) .Verifiable(); pageFactory .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Verifiable(); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); // Act 1 var result1 = viewEngine.FindView(context, "baz"); // Assert 1 Assert.True(result1.Success); var view1 = Assert.IsType(result1.View); Assert.Same(page, view1.RazorPage); pageFactory.Verify(); // Act 2 pageFactory .Setup(p => p.CreateFactory(It.IsAny())) .Throws(new Exception("Shouldn't be called")); var result2 = viewEngine.FindView(context, "baz"); // Assert 2 Assert.True(result2.Success); var view2 = Assert.IsType(result2.View); Assert.Same(page, view2.RazorPage); pageFactory.Verify(); } [Fact] public void FindView_InvokesPageFactoryIfChangeTokenExpired() { // Arrange var page1 = Mock.Of(); var page2 = Mock.Of(); var sequence = new MockSequence(); var cancellationTokenSource = new CancellationTokenSource(); var changeToken = new CancellationChangeToken(cancellationTokenSource.Token); var pageFactory = new Mock(); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Returns(new RazorPageFactoryResult(new[] { changeToken })); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml")) .Returns(new RazorPageFactoryResult(() => page1, new IChangeToken[0])) .Verifiable(); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Returns(new RazorPageFactoryResult(() => page2, new IChangeToken[0])); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); // Act 1 var result1 = viewEngine.FindView(context, "baz"); // Assert 1 Assert.True(result1.Success); var view1 = Assert.IsType(result1.View); Assert.Same(page1, view1.RazorPage); // Act 2 cancellationTokenSource.Cancel(); var result2 = viewEngine.FindView(context, "baz"); // Assert 2 Assert.True(result2.Success); var view2 = Assert.IsType(result2.View); Assert.Same(page2, view2.RazorPage); pageFactory.Verify(); } [Fact] public void FindView_InvokesPageFactoryIfViewStartExpirationTokensHaveExpired() { // Arrange var page1 = Mock.Of(); var page2 = Mock.Of(); var viewStart = Mock.Of(); var sequence = new MockSequence(); var cancellationTokenSource = new CancellationTokenSource(); var changeToken = new CancellationChangeToken(cancellationTokenSource.Token); var pageFactory = new Mock(); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Returns(new RazorPageFactoryResult(() => page1, new IChangeToken[0])); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(new[] { changeToken })) .Verifiable(); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) .Returns(new RazorPageFactoryResult(() => page2, new IChangeToken[0])); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) .Returns(new RazorPageFactoryResult(() => viewStart, new IChangeToken[0])); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); // Act 1 var result1 = viewEngine.FindView(context, "baz"); // Assert 1 Assert.True(result1.Success); var view1 = Assert.IsType(result1.View); Assert.Same(page1, view1.RazorPage); Assert.Empty(view1.ViewStartPages); // Act 2 cancellationTokenSource.Cancel(); var result2 = viewEngine.FindView(context, "baz"); // Assert 2 Assert.True(result2.Success); var view2 = Assert.IsType(result2.View); Assert.Same(page2, view2.RazorPage); var actualViewStart = Assert.Single(view2.ViewStartPages); Assert.Equal(viewStart, actualViewStart); 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(); var expander = new Mock(); var expandedLocations = new[] { "viewlocation1", "viewlocation2", "viewlocation3", }; expander .Setup(v => v.PopulateValues(It.IsAny())) .Callback((ViewLocationExpanderContext expanderContext) => { expanderContext.Values["somekey"] = "somevalue"; }) .Verifiable(); expander .Setup(v => v.ExpandViewLocations( It.IsAny(), It.IsAny>())) .Returns(expandedLocations) .Verifiable(); var viewEngine = CreateViewEngine( pageFactory, expanders: new[] { expander.Object }); var context = GetActionContext(_controllerTestContext); // Act - 1 var result = viewEngine.FindView(context, "myview"); // Assert - 1 Assert.False(result.Success); Assert.Equal(expandedLocations, result.SearchedLocations); expander.Verify(); // Act - 2 result = viewEngine.FindView(context, "myview"); // Assert - 2 Assert.False(result.Success); Assert.Equal(expandedLocations, result.SearchedLocations); expander.Verify( v => v.PopulateValues(It.IsAny()), Times.Exactly(2)); expander.Verify( v => v.ExpandViewLocations(It.IsAny(), It.IsAny>()), Times.Once()); } [Fact] public void FindView_InvokesViewLocationExpanders_IfChangeTokenExpires() { // Arrange var cancellationTokenSource = new CancellationTokenSource(); var changeToken = new CancellationChangeToken(cancellationTokenSource.Token); var page = Mock.Of(); var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("viewlocation3")) .Returns(new RazorPageFactoryResult(new[] { changeToken })); var expander = new Mock(); var expandedLocations = new[] { "viewlocation1", "viewlocation2", "viewlocation3", }; expander .Setup(v => v.PopulateValues(It.IsAny())) .Callback((ViewLocationExpanderContext expanderContext) => { expanderContext.Values["somekey"] = "somevalue"; }) .Verifiable(); expander .Setup(v => v.ExpandViewLocations( It.IsAny(), It.IsAny>())) .Returns(expandedLocations) .Verifiable(); var viewEngine = CreateViewEngine( pageFactory.Object, expanders: new[] { expander.Object }); var context = GetActionContext(_controllerTestContext); // Act - 1 var result = viewEngine.FindView(context, "MyView"); // Assert - 1 Assert.False(result.Success); Assert.Equal(expandedLocations, result.SearchedLocations); expander.Verify(); // Act - 2 pageFactory .Setup(p => p.CreateFactory("viewlocation3")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); cancellationTokenSource.Cancel(); result = viewEngine.FindView(context, "MyView"); // Assert - 2 Assert.True(result.Success); var view = Assert.IsType(result.View); Assert.Same(page, view.RazorPage); expander.Verify( v => v.PopulateValues(It.IsAny()), Times.Exactly(2)); expander.Verify( v => v.ExpandViewLocations(It.IsAny(), It.IsAny>()), Times.Exactly(2)); } [Theory] [InlineData(null)] [InlineData("")] public void FindPage_ThrowsIfNameIsNullOrEmpty(string pageName) { // Arrange var viewEngine = CreateViewEngine(); var context = GetActionContext(_controllerTestContext); // Act & Assert ExceptionAssert.ThrowsArgumentNullOrEmpty(() => viewEngine.FindPage(context, pageName), "pageName"); } [Theory] [MemberData(nameof(ViewLocationExpanderTestData))] public void FindPage_UsesViewLocationExpander_ToExpandPaths( IDictionary routeValues, IEnumerable expectedSeeds) { // Arrange var page = Mock.Of(); var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("expanded-path/bar-layout")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Verifiable(); var expander = new Mock(); expander .Setup(e => e.PopulateValues(It.IsAny())) .Callback((ViewLocationExpanderContext c) => { Assert.NotNull(c.ActionContext); c.Values["expander-key"] = expander.ToString(); }) .Verifiable(); expander .Setup(e => e.ExpandViewLocations( It.IsAny(), It.IsAny>())) .Returns((ViewLocationExpanderContext c, IEnumerable 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 page = Mock.Of(); 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); } [Theory] // Looks in RouteValueDefaults [InlineData(true)] // Looks in RouteConstraints [InlineData(false)] public void FindPage_SelectsActionCaseInsensitively(bool isAttributeRouted) { // 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 { { "controller", "foo" } }; var page = new Mock(MockBehavior.Strict).Object; var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("/Views/Foo/details.cshtml")) .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) .Verifiable(); var viewEngine = CreateViewEngine(pageFactory.Object); var routesInActionDescriptor = new Dictionary() { { "controller", "Foo" } }; var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted); // Act var result = viewEngine.FindPage(context, "details"); // Assert Assert.Equal("details", result.Name); Assert.Same(page, result.Page); Assert.Null(result.SearchedLocations); pageFactory.Verify(); } [Theory] // Looks in RouteValueDefaults [InlineData(true)] // Looks in RouteConstraints [InlineData(false)] public void FindPage_LooksForPages_UsingActionDescriptor_Controller(bool isAttributeRouted) { // Arrange var expected = new[] { "/Views/bar/foo.cshtml", "/Views/Shared/foo.cshtml", }; var routeValues = new Dictionary { { "controller", "Bar" } }; var routesInActionDescriptor = new Dictionary() { { "controller", "bar" } }; var page = Mock.Of(); var viewEngine = CreateViewEngine(); var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted); // Act var result = viewEngine.FindPage(context, "foo"); // Assert Assert.Equal("foo", result.Name); Assert.Null(result.Page); Assert.Equal(expected, result.SearchedLocations); } [Theory] // Looks in RouteValueDefaults [InlineData(true)] // Looks in RouteConstraints [InlineData(false)] public void FindPage_LooksForPages_UsingActionDescriptor_Areas(bool isAttributeRouted) { // Arrange var expected = new[] { "/Areas/world/Views/bar/foo.cshtml", "/Areas/world/Views/Shared/foo.cshtml", "/Views/Shared/foo.cshtml" }; var routeValues = new Dictionary { { "controller", "Bar" }, { "area", "World" } }; var routesInActionDescriptor = new Dictionary() { { "controller", "bar" }, { "area", "world" } }; var page = Mock.Of(); var viewEngine = CreateViewEngine(); var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted); // Act var result = viewEngine.FindPage(context, "foo"); // Assert Assert.Equal("foo", result.Name); Assert.Null(result.Page); Assert.Equal(expected, result.SearchedLocations); } [Theory] [InlineData(true)] [InlineData(false)] public void FindPage_LooksForPages_UsesRouteValuesAsFallback(bool isAttributeRouted) { // Arrange var expected = new[] { "/Views/foo/bar.cshtml", "/Views/Shared/bar.cshtml", }; var routeValues = new Dictionary() { { "controller", "foo" } }; var page = Mock.Of(); var viewEngine = CreateViewEngine(); var context = GetActionContextWithActionDescriptor(routeValues, new Dictionary(), isAttributeRouted); // Act var result = viewEngine.FindPage(context, "bar"); // Assert Assert.Equal("bar", result.Name); Assert.Null(result.Page); Assert.Equal(expected, result.SearchedLocations); } [Fact] public void AreaViewLocationFormats_ContainsExpectedLocations() { // Arrange var viewEngine = CreateViewEngine(); var areaViewLocations = new string[] { "/Areas/{2}/Views/{1}/{0}.cshtml", "/Areas/{2}/Views/Shared/{0}.cshtml", "/Views/Shared/{0}.cshtml" }; // Act & Assert Assert.Equal(areaViewLocations, viewEngine.AreaViewLocationFormats); } [Fact] public void ViewLocationFormats_ContainsExpectedLocations() { // Arrange var viewEngine = CreateViewEngine(); var viewLocations = new string[] { "/Views/{1}/{0}.cshtml", "/Views/Shared/{0}.cshtml" }; // Act & Assert Assert.Equal(viewLocations, viewEngine.ViewLocationFormats); } [Fact] public void GetNormalizedRouteValue_ReturnsValueFromRouteConstraints_IfKeyHandlingIsRequired() { // Arrange var key = "some-key"; var actionDescriptor = new ActionDescriptor { RouteConstraints = new[] { new RouteDataActionConstraint(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] public void GetNormalizedRouteValue_ReturnsRouteValue_IfValueDoesNotMatchRouteConstraint() { // Arrange var key = "some-key"; var actionDescriptor = new ActionDescriptor { RouteConstraints = new[] { new RouteDataActionConstraint(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_ReturnsNull_IfRouteConstraintKeyHandlingIsDeny() { // Arrange var key = "some-key"; var actionDescriptor = new ActionDescriptor { RouteConstraints = new[] { new RouteDataActionConstraint(key, routeValue: string.Empty) } }; var actionContext = new ActionContext { ActionDescriptor = actionDescriptor, RouteData = new RouteData() }; actionContext.RouteData.Values[key] = "route-value"; // Act var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); // Assert Assert.Null(result); } [Fact] public void GetNormalizedRouteValue_ReturnsRouteDataValue_IfRouteConstraintKeyHandlingIsCatchAll() { // Arrange var key = "some-key"; var actionDescriptor = new ActionDescriptor { RouteConstraints = new[] { RouteDataActionConstraint.CreateCatchAll(key) } }; 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_UsesRouteValueDefaults_IfAttributeRouted() { // Arrange var key = "some-key"; var actionDescriptor = new ActionDescriptor { AttributeRouteInfo = new AttributeRouteInfo(), }; actionDescriptor.RouteValueDefaults[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] public void GetNormalizedRouteValue_UsesRouteValue_IfRouteValueDefaultsDoesNotMatchRouteValue() { // Arrange var key = "some-key"; var actionDescriptor = new ActionDescriptor { AttributeRouteInfo = new AttributeRouteInfo(), }; actionDescriptor.RouteValueDefaults[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_ConvertsRouteDefaultToStringValue_IfAttributeRouted() { using (new CultureReplacer()) { // Arrange var key = "some-key"; var actionDescriptor = new ActionDescriptor { AttributeRouteInfo = new AttributeRouteInfo(), }; actionDescriptor.RouteValueDefaults[key] = 32; var actionContext = new ActionContext { ActionDescriptor = actionDescriptor, RouteData = new RouteData() }; actionContext.RouteData.Values[key] = 32; // Act var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); // Assert Assert.Equal("32", result); } } [Fact] public void GetNormalizedRouteValue_UsesRouteDataValue_IfKeyDoesNotExistInRouteDefaultValues() { // 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] = "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); } } private TestableRazorViewEngine CreateViewEngine( IRazorPageFactoryProvider pageFactory = null, IEnumerable expanders = null) { pageFactory = pageFactory ?? Mock.Of(); return new TestableRazorViewEngine( pageFactory, GetOptionsAccessor(expanders)); } private static IOptions GetOptionsAccessor( IEnumerable expanders = null) { var options = new RazorViewEngineOptions(); if (expanders != null) { foreach (var expander in expanders) { options.ViewLocationExpanders.Add(expander); } } var optionsAccessor = new Mock>(); optionsAccessor.SetupGet(v => v.Value) .Returns(options); return optionsAccessor.Object; } private static ActionContext GetActionContext(IDictionary routeValues) { var httpContext = new DefaultHttpContext(); var routeData = new RouteData(); foreach (var kvp in routeValues) { routeData.Values.Add(kvp.Key, kvp.Value); } var actionDesciptor = new ActionDescriptor(); actionDesciptor.RouteConstraints = new List(); return new ActionContext(httpContext, routeData, actionDesciptor); } private static ActionContext GetActionContextWithActionDescriptor( IDictionary routeValues, IDictionary routesInActionDescriptor, bool isAttributeRouted) { var httpContext = new DefaultHttpContext(); var routeData = new RouteData(); foreach (var kvp in routeValues) { routeData.Values.Add(kvp.Key, kvp.Value); } var actionDescriptor = new ActionDescriptor(); if (isAttributeRouted) { actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo(); foreach (var kvp in routesInActionDescriptor) { actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value); } } else { actionDescriptor.RouteConstraints = new List(); foreach (var kvp in routesInActionDescriptor) { actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(kvp.Key, kvp.Value)); } } return new ActionContext(httpContext, routeData, actionDescriptor); } private class TestableRazorViewEngine : RazorViewEngine { private IEnumerable _viewLocationFormats; private IEnumerable _areaViewLocationFormats; public TestableRazorViewEngine( IRazorPageFactoryProvider pageFactory, IOptions optionsAccessor) : base(pageFactory, Mock.Of(), new HtmlTestEncoder(), optionsAccessor) { } public void SetLocationFormats( IEnumerable viewLocationFormats, IEnumerable areaViewLocationFormats) { _viewLocationFormats = viewLocationFormats; _areaViewLocationFormats = areaViewLocationFormats; } public override IEnumerable ViewLocationFormats => _viewLocationFormats != null ? _viewLocationFormats : base.ViewLocationFormats; public override IEnumerable AreaViewLocationFormats => _areaViewLocationFormats != null ? _areaViewLocationFormats : base.AreaViewLocationFormats; public IMemoryCache ViewLookupCachePublic => ViewLookupCache; } } }