// 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.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Xunit; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class DefaultPageLoaderTest { [Fact] public void CreateDescriptor_CopiesPropertiesFromBaseClass() { // Arrange var expected = new PageActionDescriptor() // We only copy the properties that are meaningful for pages. { ActionConstraints = new List(), AttributeRouteInfo = new AttributeRouteInfo(), FilterDescriptors = new List(), RelativePath = "/Foo", RouteValues = new Dictionary(), ViewEnginePath = "/Pages/Foo", }; // Act var actual = DefaultPageLoader.CreateDescriptor(expected, new RazorPageAttribute(expected.RelativePath, typeof(EmptyPage), "")); // Assert Assert.Same(expected.ActionConstraints, actual.ActionConstraints); Assert.Same(expected.AttributeRouteInfo, actual.AttributeRouteInfo); Assert.Same(expected.FilterDescriptors, actual.FilterDescriptors); Assert.Same(expected.Properties, actual.Properties); Assert.Same(expected.RelativePath, actual.RelativePath); Assert.Same(expected.RouteValues, actual.RouteValues); Assert.Same(expected.ViewEnginePath, actual.ViewEnginePath); } // We want to test the the 'empty' page has no bound properties, and no handler methods. [Fact] public void CreateDescriptor_EmptyPage() { // Arrange var type = typeof(EmptyPage); // Act var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), new RazorPageAttribute("/Pages/Index", type, "")); // Assert Assert.Empty(result.BoundProperties); Assert.Empty(result.HandlerMethods); Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.HandlerTypeInfo); Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.ModelTypeInfo); Assert.Same(typeof(EmptyPage).GetTypeInfo(), result.PageTypeInfo); } // We want to test the the 'empty' page and pagemodel has no bound properties, and no handler methods. [Fact] public void CreateDescriptor_EmptyPageModel() { // Arrange var type = typeof(EmptyPageWithPageModel); // Act var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), new RazorPageAttribute("/Pages/Index", type, "")); // Assert Assert.Empty(result.BoundProperties); Assert.Empty(result.HandlerMethods); Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), result.HandlerTypeInfo); Assert.Same(typeof(EmptyPageModel).GetTypeInfo(), result.ModelTypeInfo); Assert.Same(typeof(EmptyPageWithPageModel).GetTypeInfo(), result.PageTypeInfo); } private class EmptyPage : Page { // Copied from generated code [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => null; public EmptyPage Model => ViewData.Model; public override Task ExecuteAsync() { throw new NotImplementedException(); } } private class EmptyPageWithPageModel : Page { // Copied from generated code [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } public global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary ViewData => null; public EmptyPageModel Model => ViewData.Model; public override Task ExecuteAsync() { throw new NotImplementedException(); } } private class EmptyPageModel : PageModel { } [Fact] // If the model has handler methods, we prefer those. public void CreateDescriptor_FindsHandlerMethod_OnModel() { // Arrange var type = typeof(PageWithHandlerThatGetsIgnored); // Act var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), new RazorPageAttribute("/Pages/Index", type, "")); // Assert Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name)); Assert.Collection(result.HandlerMethods, h => Assert.Equal("OnGet", h.MethodInfo.Name)); Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), result.HandlerTypeInfo); Assert.Same(typeof(ModelWithHandler).GetTypeInfo(), result.ModelTypeInfo); Assert.Same(typeof(PageWithHandlerThatGetsIgnored).GetTypeInfo(), result.PageTypeInfo); } private class ModelWithHandler { [ModelBinder] public int BindMe { get; set; } public void OnGet() { } } private class PageWithHandlerThatGetsIgnored { public ModelWithHandler Model => null; [ModelBinder] public int IgnoreMe { get; set; } public void OnPost() { } } [Fact] // If the model has no handler methods, we look at the page instead. public void CreateDescriptor_FindsHandlerMethodOnPage_WhenModelHasNoHandlers() { // Arrange var type = typeof(PageWithHandler); // Act var result = DefaultPageLoader.CreateDescriptor(new PageActionDescriptor(), new RazorPageAttribute("/Pages/Index", type, "")); // Assert Assert.Collection(result.BoundProperties, p => Assert.Equal("BindMe", p.Name)); Assert.Collection(result.HandlerMethods, h => Assert.Equal("OnGet", h.MethodInfo.Name)); Assert.Same(typeof(PageWithHandler).GetTypeInfo(), result.HandlerTypeInfo); Assert.Same(typeof(PocoModel).GetTypeInfo(), result.ModelTypeInfo); Assert.Same(typeof(PageWithHandler).GetTypeInfo(), result.PageTypeInfo); } private class PocoModel { // Just a plain ol' model, nothing to see here. [ModelBinder] public int IgnoreMe { get; set; } } private class PageWithHandler { public PocoModel Model => null; [ModelBinder] public int BindMe { get; set; } public void OnGet() { } } [Fact] public void CreateHandlerMethods_DiscoversHandlersFromBaseType() { // Arrange var type = typeof(InheritsMethods).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateHandlerMethods(type); // Assert Assert.Collection( results.OrderBy(h => h.MethodInfo.Name).ToArray(), (handler) => { Assert.Equal("OnGet", handler.MethodInfo.Name); Assert.Equal(typeof(InheritsMethods), handler.MethodInfo.DeclaringType); }, (handler) => { Assert.Equal("OnGet", handler.MethodInfo.Name); Assert.Equal(typeof(TestSetPageModel), handler.MethodInfo.DeclaringType); }, (handler) => { Assert.Equal("OnPost", handler.MethodInfo.Name); Assert.Equal(typeof(TestSetPageModel), handler.MethodInfo.DeclaringType); }); } private class TestSetPageModel { public void OnGet() { } public void OnPost() { } } private class TestSetPageWithModel { public TestSetPageModel Model { get; set; } } private class InheritsMethods : TestSetPageModel { public new void OnGet() { } } [Fact] public void CreateHandlerMethods_IgnoresNonPublicMethods() { // Arrange var type = typeof(ProtectedModel).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateHandlerMethods(type); // Assert Assert.Empty(results); } private class ProtectedModel { protected void OnGet() { } private void OnPost() { } } [Fact] public void CreateHandlerMethods_IgnoreGenericTypeParameters() { // Arrange var type = typeof(GenericClassModel).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateHandlerMethods(type); // Assert Assert.Empty(results); } private class GenericClassModel { public void OnGet() { } } [Fact] public void CreateHandlerMethods_IgnoresStaticMethods() { // Arrange var type = typeof(PageModelWithStaticHandler).GetTypeInfo(); var expected = type.GetMethod(nameof(PageModelWithStaticHandler.OnGet), BindingFlags.Public | BindingFlags.Instance); // Act var results = DefaultPageLoader.CreateHandlerMethods(type); // Assert Assert.Collection( results, handler => Assert.Same(expected, handler.MethodInfo)); } private class PageModelWithStaticHandler { public static void OnGet(string name) { } public void OnGet() { } } [Fact] public void CreateHandlerMethods_IgnoresAbstractMethods() { // Arrange var type = typeof(PageModelWithAbstractMethod).GetTypeInfo(); var expected = type.GetMethod(nameof(PageModelWithAbstractMethod.OnGet), BindingFlags.Public | BindingFlags.Instance); // Act var results = DefaultPageLoader.CreateHandlerMethods(type); // Assert Assert.Collection( results, handler => Assert.Same(expected, handler.MethodInfo)); } private abstract class PageModelWithAbstractMethod { public abstract void OnPost(string name); public void OnGet() { } } [Fact] public void CreateHandlerMethods_IgnoresMethodWithNonHandlerAttribute() { // Arrange var type = typeof(PageWithNonHandlerMethod).GetTypeInfo(); var expected = type.GetMethod(nameof(PageWithNonHandlerMethod.OnGet), BindingFlags.Public | BindingFlags.Instance); // Act var results = DefaultPageLoader.CreateHandlerMethods(type); // Assert Assert.Collection( results, handler => Assert.Same(expected, handler.MethodInfo)); } private class PageWithNonHandlerMethod { [NonHandler] public void OnPost(string name) { } public void OnGet() { } } // There are more tests for the parsing elsewhere, this is just testing that it's wired // up to the descriptor. [Fact] public void CreateHandlerMethods_ParsesMethod() { // Arrange var type = typeof(PageModelWithHandlerNames).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateHandlerMethods(type); // Assert Assert.Collection( results.OrderBy(h => h.MethodInfo.Name), handler => { Assert.Same(type.GetMethod(nameof(PageModelWithHandlerNames.OnPutDeleteAsync)), handler.MethodInfo); Assert.Equal("Put", handler.HttpMethod); Assert.Equal("Delete", handler.Name.ToString()); }); } private class PageModelWithHandlerNames { public void OnPutDeleteAsync() { } public void Foo() // This isn't a valid handler name. { } } [Fact] public void CreateHandlerMethods_AddsParameterDescriptors() { // Arrange var type = typeof(PageWithHandlerParameters).GetTypeInfo(); var expected = type.GetMethod(nameof(PageWithHandlerParameters.OnPost), BindingFlags.Public | BindingFlags.Instance); // Act var results = DefaultPageLoader.CreateHandlerMethods(type); // Assert var handler = Assert.Single(results); Assert.Collection( handler.Parameters, p => { Assert.Equal(typeof(string), p.ParameterType); Assert.NotNull(p.ParameterInfo); Assert.Equal("name", p.Name); }, p => { Assert.Equal(typeof(int), p.ParameterType); Assert.NotNull(p.ParameterInfo); Assert.Equal("id", p.Name); Assert.Equal("personId", p.BindingInfo.BinderModelName); }); } private class PageWithHandlerParameters { public void OnPost(string name, [ModelBinder(Name = "personId")] int id) { } } // We're using PropertyHelper from Common to find the properties here, which implements // out standard set of semantics for properties that the framework interacts with. // // One of the desirable consequences of that is we only find 'visible' properties. We're not // retesting all of the details of PropertyHelper here, just the visibility part as a quick check // that we're using PropertyHelper as expected. [Fact] public void CreateBoundProperties_UsesPropertyHelpers_ToFindProperties() { // Arrange var type = typeof(HidesAProperty).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateBoundProperties(type); // Assert Assert.Collection( results.OrderBy(p => p.Property.Name), p => { Assert.Equal(typeof(HidesAProperty).GetTypeInfo(), p.Property.DeclaringType.GetTypeInfo()); }); } private class HasAHiddenProperty { [BindProperty] public int Property { get; set; } } private class HidesAProperty : HasAHiddenProperty { [BindProperty] public new int Property { get; set; } } // We're using BindingInfo to make property binding opt-in here. We're not going to retest // all of the semantics of BindingInfo here, as that's covered elsewhere. [Fact] public void CreateBoundProperties_UsesBindingInfo_ToFindProperties() { // Arrange var type = typeof(ModelWithBindingInfoProperty).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateBoundProperties(type); // Assert Assert.Collection( results.OrderBy(p => p.Property.Name), p => { Assert.Equal("Property", p.Property.Name); }); } private class ModelWithBindingInfoProperty { [ModelBinder] public int Property { get; set; } public int IgnoreMe { get; set; } } // Additionally [BindProperty] on a property can opt-in a property [Fact] public void CreateBoundProperties_UsesBindPropertyAttribute_ToFindProperties() { // Arrange var type = typeof(ModelWithBindProperty).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateBoundProperties(type); // Assert Assert.Collection( results.OrderBy(p => p.Property.Name), p => { Assert.Equal("Property", p.Property.Name); }); } private class ModelWithBindProperty { [BindProperty] public int Property { get; set; } public int IgnoreMe { get; set; } } // Additionally [BindProperty] on a property can opt-in a property [Fact] public void CreateBoundProperties_BindPropertyAttributeOnModel_OptsInAllProperties() { // Arrange var type = typeof(ModelWithBindPropertyOnClass).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateBoundProperties(type); // Assert Assert.Collection( results.OrderBy(p => p.Property.Name), p => { Assert.Equal("Property", p.Property.Name); }); } [BindProperty] private class ModelWithBindPropertyOnClass : EmptyPageModel { public int Property { get; set; } } [Fact] public void CreateBoundProperties_SupportsGet_OnProperty() { // Arrange var type = typeof(ModelSupportsGetOnProperty).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateBoundProperties(type); // Assert Assert.Collection( results.OrderBy(p => p.Property.Name), p => { Assert.Equal("Property", p.Property.Name); Assert.NotNull(p.BindingInfo.RequestPredicate); Assert.True(p.BindingInfo.RequestPredicate(new ActionContext() { HttpContext = new DefaultHttpContext() { Request = { Method ="GET", } } })); }); } private class ModelSupportsGetOnProperty { [BindProperty(SupportsGet = true)] public int Property { get; set; } public int IgnoreMe { get; set; } } [Fact] public void CreateBoundProperties_SupportsGet_OnClass() { // Arrange var type = typeof(ModelSupportsGetOnClass).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateBoundProperties(type); // Assert Assert.Collection( results.OrderBy(p => p.Property.Name), p => { Assert.Equal("Property", p.Property.Name); Assert.NotNull(p.BindingInfo.RequestPredicate); Assert.True(p.BindingInfo.RequestPredicate(new ActionContext() { HttpContext = new DefaultHttpContext() { Request = { Method ="GET", } } })); }); } [BindProperty(SupportsGet = true)] private class ModelSupportsGetOnClass : EmptyPageModel { public int Property { get; set; } } [Fact] public void CreateBoundProperties_SupportsGet_Override() { // Arrange var type = typeof(ModelSupportsGetOverride).GetTypeInfo(); // Act var results = DefaultPageLoader.CreateBoundProperties(type); // Assert Assert.Collection( results.OrderBy(p => p.Property.Name), p => { Assert.Equal("Property", p.Property.Name); Assert.NotNull(p.BindingInfo.RequestPredicate); Assert.False(p.BindingInfo.RequestPredicate(new ActionContext() { HttpContext = new DefaultHttpContext() { Request = { Method ="GET", } } })); }); } [BindProperty(SupportsGet = true)] private class ModelSupportsGetOverride : EmptyPageModel { [BindProperty(SupportsGet = false)] public int Property { get; set; } } [Theory] [InlineData("Foo")] [InlineData("On")] [InlineData("OnAsync")] [InlineData("Async")] public void TryParseHandler_ParsesHandlerNames_InvalidData(string methodName) { // Arrange // Act var result = DefaultPageLoader.TryParseHandlerMethod(methodName, out var httpMethod, out var handler); // Assert Assert.False(result); Assert.Null(httpMethod); Assert.Null(handler); } [Theory] [InlineData("OnG", "G", null)] [InlineData("OnGAsync", "G", null)] [InlineData("OnPOST", "P", "OST")] [InlineData("OnPOSTAsync", "P", "OST")] [InlineData("OnDeleteFoo", "Delete", "Foo")] [InlineData("OnDeleteFooAsync", "Delete", "Foo")] [InlineData("OnMadeupLongHandlerName", "Madeup", "LongHandlerName")] [InlineData("OnMadeupLongHandlerNameAsync", "Madeup", "LongHandlerName")] public void TryParseHandler_ParsesHandlerNames_ValidData(string methodName, string expectedHttpMethod, string expectedHandler) { // Arrange // Act var result = DefaultPageLoader.TryParseHandlerMethod(methodName, out var httpMethod, out var handler); // Assert Assert.True(result); Assert.Equal(expectedHttpMethod, httpMethod); Assert.Equal(expectedHandler, handler); } } }