Add integration and functional tests of `[BindRequired]` on page properties (#8677)

- #7353
This commit is contained in:
Doug Bunting 2018-10-31 14:15:14 -07:00 committed by GitHub
parent f2af66bc31
commit a6199bbfba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 145 additions and 24 deletions

View File

@ -4,4 +4,5 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.RazorPages.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -4,7 +4,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
@ -420,14 +419,16 @@ Hello from /Pages/Shared/";
var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), ""); var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), "");
var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage); var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage);
var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel"); var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel")
message.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{ {
["__RequestVerificationToken"] = token, Content = new FormUrlEncodedContent(new Dictionary<string, string>
["ConfirmPassword"] = "", {
["Password"] = "", ["__RequestVerificationToken"] = token,
["Email"] = "" ["ConfirmPassword"] = "",
}); ["Password"] = "",
["Email"] = ""
})
};
message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}"); message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}");
// Act // Act
@ -443,18 +444,20 @@ Hello from /Pages/Shared/";
public async Task PageConventions_CustomizedModelCanWorkWithModelState() public async Task PageConventions_CustomizedModelCanWorkWithModelState()
{ {
// Arrange // Arrange
var getPage = await Client.GetAsync("/CustomModelTypeModel"); var getPage = await Client.GetAsync("/CustomModelTypeModel?Attempts=0");
var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), ""); var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), "");
var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage); var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage);
var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel"); var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel?Attempts=3")
message.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{ {
["__RequestVerificationToken"] = token, Content = new FormUrlEncodedContent(new Dictionary<string, string>
["Email"] = "javi@example.com", {
["Password"] = "Password.12$", ["__RequestVerificationToken"] = token,
["ConfirmPassword"] = "Password.12$", ["Email"] = "javi@example.com",
}); ["Password"] = "Password.12$",
["ConfirmPassword"] = "Password.12$",
})
};
message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}"); message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}");
// Act // Act
@ -465,6 +468,37 @@ Hello from /Pages/Shared/";
Assert.Equal("/", response.Headers.Location.ToString()); Assert.Equal("/", response.Headers.Location.ToString());
} }
[Fact]
public async Task PageConventions_CustomizedModelCanWorkWithModelState_EnforcesBindRequired()
{
// Arrange
var getPage = await Client.GetAsync("/CustomModelTypeModel?Attempts=0");
var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(await getPage.Content.ReadAsStringAsync(), "");
var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(getPage);
var message = new HttpRequestMessage(HttpMethod.Post, "/CustomModelTypeModel")
{
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["__RequestVerificationToken"] = token,
["Email"] = "javi@example.com",
["Password"] = "Password.12$",
["ConfirmPassword"] = "Password.12$",
})
};
message.Headers.TryAddWithoutValidation("Cookie", $"{cookie.Key}={cookie.Value}");
// Act
var response = await Client.SendAsync(message);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseText = await response.Content.ReadAsStringAsync();
Assert.Contains(
"A value for the &#x27;Attempts&#x27; parameter or property was not provided.",
responseText);
}
[Fact] [Fact]
public async Task ValidationAttributes_OnTopLevelProperties() public async Task ValidationAttributes_OnTopLevelProperties()
{ {
@ -642,10 +676,12 @@ Hello from /Pages/Shared/";
var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(response); var cookie = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(response);
var content = new MultipartFormDataContent(); var content = new MultipartFormDataContent
content.Add(new StringContent("property1-value"), property1); {
content.Add(new StringContent("test-value1"), file1, "test1.txt"); { new StringContent("property1-value"), property1 },
content.Add(new StringContent("test-value2"), file3, "test2.txt"); { new StringContent("test-value1"), file1, "test1.txt" },
{ new StringContent("test-value2"), file3, "test2.txt" }
};
var request = new HttpRequestMessage(HttpMethod.Post, url) var request = new HttpRequestMessage(HttpMethod.Post, url)
{ {

View File

@ -1,15 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Primitives; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.IntegrationTests namespace Microsoft.AspNetCore.Mvc.IntegrationTests
@ -179,6 +181,74 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
} }
} }
[Theory]
[InlineData(null, false)]
[InlineData(123, true)]
public async Task BindModelAsync_WithBindPageProperty_EnforcesBindRequired(int? input, bool isValid)
{
// Arrange
var propertyInfo = typeof(TestPage).GetProperty(nameof(TestPage.BindRequiredProperty));
var propertyDescriptor = new PageBoundPropertyDescriptor
{
BindingInfo = BindingInfo.GetBindingInfo(new[]
{
new FromQueryAttribute { Name = propertyInfo.Name },
}),
Name = propertyInfo.Name,
ParameterType = propertyInfo.PropertyType,
Property = propertyInfo,
};
var typeInfo = typeof(TestPage).GetTypeInfo();
var actionDescriptor = new CompiledPageActionDescriptor
{
BoundProperties = new[] { propertyDescriptor },
HandlerTypeInfo = typeInfo,
ModelTypeInfo = typeInfo,
PageTypeInfo = typeInfo,
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
request.Method = "POST";
if (input.HasValue)
{
request.QueryString = new QueryString($"?{propertyDescriptor.Name}={input.Value}");
}
});
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(modelMetadataProvider);
var modelBinderFactory = ModelBindingTestHelper.GetModelBinderFactory(modelMetadataProvider);
var modelMetadata = modelMetadataProvider
.GetMetadataForProperty(typeof(TestPage), propertyDescriptor.Name);
var pageBinder = PageBinderFactory.CreatePropertyBinder(
parameterBinder,
modelMetadataProvider,
modelBinderFactory,
actionDescriptor);
var pageContext = new PageContext
{
ActionDescriptor = actionDescriptor,
HttpContext = testContext.HttpContext,
RouteData = testContext.RouteData,
ValueProviderFactories = testContext.ValueProviderFactories,
};
var page = new TestPage();
// Act
await pageBinder(pageContext, page);
// Assert
Assert.Equal(isValid, pageContext.ModelState.IsValid);
if (isValid)
{
Assert.Equal(input.Value, page.BindRequiredProperty);
}
}
[Theory] [Theory]
[InlineData("RequiredAndStringLengthProp", null, false)] [InlineData("RequiredAndStringLengthProp", null, false)]
[InlineData("RequiredAndStringLengthProp", "", false)] [InlineData("RequiredAndStringLengthProp", "", false)]
@ -231,12 +301,18 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
} }
} }
class TestController private class TestController
{ {
[BindNever] public string BindNeverProp { get; set; } [BindNever] public string BindNeverProp { get; set; }
[BindRequired] public int BindRequiredProp { get; set; } [BindRequired] public int BindRequiredProp { get; set; }
[Required, StringLength(3)] public string RequiredAndStringLengthProp { get; set; } [Required, StringLength(3)] public string RequiredAndStringLengthProp { get; set; }
[DisplayName("My Display Name"), StringLength(3)] public string DisplayNameStringLengthProp { get; set; } [DisplayName("My Display Name"), StringLength(3)] public string DisplayNameStringLengthProp { get; set; }
} }
private class TestPage : PageModel
{
[BindRequired]
public int BindRequiredProperty { get; set; }
}
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -13,6 +14,10 @@ namespace RazorPagesWebSite
public string ReturnUrl { get; set; } public string ReturnUrl { get; set; }
[BindRequired]
[FromQuery(Name = nameof(Attempts))]
public int Attempts { get; set; }
public class InputModel public class InputModel
{ {
[Required] [Required]
@ -69,10 +74,13 @@ namespace RazorPagesWebSite
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
Attempts++;
RouteData.Values.Add(nameof(Attempts), Attempts);
return Page(); return Page();
} }
return Redirect("~/"); return Redirect("~/");
} }
} }
} }