diff --git a/Mvc.sln b/Mvc.sln index 414ea97b9d..4f4e861e24 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22822.1 +VisualStudioVersion = 14.0.22810.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -15,6 +15,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Razor" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Core", "src\Microsoft.AspNet.Mvc.Core\Microsoft.AspNet.Mvc.Core.xproj", "{C48DA9D7-ACB5-4408-AA79-27ECB60A67EF}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ModelBinding", "src\Microsoft.AspNet.Mvc.ModelBinding\Microsoft.AspNet.Mvc.ModelBinding.xproj", "{FA915D3D-22C3-4478-97F2-A81D28B6C503}" +EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Common", "src\Microsoft.AspNet.Mvc.Common\Microsoft.AspNet.Mvc.Common.xproj", "{F3DF6D0B-16FE-4402-B92C-7243A75CF1FD}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Razor.Test", "test\Microsoft.AspNet.Mvc.Razor.Test\Microsoft.AspNet.Mvc.Razor.Test.xproj", "{3F6E355E-4869-41D9-943B-D54771221A7F}" @@ -162,8 +164,6 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Integr EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Abstractions", "src\Microsoft.AspNet.Mvc.Abstractions\Microsoft.AspNet.Mvc.Abstractions.xproj", "{1154203C-7579-4525-906E-BC55268421C1}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Abstractions.Test", "test\Microsoft.AspNet.Mvc.Abstractions.Test\Microsoft.AspNet.Mvc.Abstractions.Test.xproj", "{DA000953-7532-4DF5-8DB9-8143DF98D999}" -EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExplorer", "src\Microsoft.AspNet.Mvc.ApiExplorer\Microsoft.AspNet.Mvc.ApiExplorer.xproj", "{A2B72833-5D70-4C42-AE85-E0319926FB8A}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExplorer.Test", "test\Microsoft.AspNet.Mvc.ApiExplorer.Test\Microsoft.AspNet.Mvc.ApiExplorer.Test.xproj", "{4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6}" @@ -208,6 +208,16 @@ Global {C48DA9D7-ACB5-4408-AA79-27ECB60A67EF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {C48DA9D7-ACB5-4408-AA79-27ECB60A67EF}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C48DA9D7-ACB5-4408-AA79-27ECB60A67EF}.Release|x86.ActiveCfg = Release|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Debug|x86.ActiveCfg = Debug|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Release|Any CPU.Build.0 = Release|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FA915D3D-22C3-4478-97F2-A81D28B6C503}.Release|x86.ActiveCfg = Release|Any CPU {F3DF6D0B-16FE-4402-B92C-7243A75CF1FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F3DF6D0B-16FE-4402-B92C-7243A75CF1FD}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3DF6D0B-16FE-4402-B92C-7243A75CF1FD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -974,18 +984,6 @@ Global {1154203C-7579-4525-906E-BC55268421C1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {1154203C-7579-4525-906E-BC55268421C1}.Release|x86.ActiveCfg = Release|Any CPU {1154203C-7579-4525-906E-BC55268421C1}.Release|x86.Build.0 = Release|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|x86.ActiveCfg = Debug|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Debug|x86.Build.0 = Debug|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Any CPU.Build.0 = Release|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|x86.ActiveCfg = Release|Any CPU - {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|x86.Build.0 = Release|Any CPU {A2B72833-5D70-4C42-AE85-E0319926FB8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2B72833-5D70-4C42-AE85-E0319926FB8A}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2B72833-5D70-4C42-AE85-E0319926FB8A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -1018,6 +1016,7 @@ Global {079EFA1F-0B0A-4853-B27B-5780D111CD85} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {314E9AD6-2FFC-4A92-A8AD-510658C64F1E} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {C48DA9D7-ACB5-4408-AA79-27ECB60A67EF} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {FA915D3D-22C3-4478-97F2-A81D28B6C503} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {F3DF6D0B-16FE-4402-B92C-7243A75CF1FD} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {3F6E355E-4869-41D9-943B-D54771221A7F} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {A8AA326E-8EE8-4F11-B750-23028E0949D7} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} @@ -1089,7 +1088,6 @@ Global {DAB1252D-577C-4912-98BE-1A812BF83F86} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {864FA09D-1E48-403A-A6C8-4F079D2A30F0} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {1154203C-7579-4525-906E-BC55268421C1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} - {DA000953-7532-4DF5-8DB9-8143DF98D999} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {A2B72833-5D70-4C42-AE85-E0319926FB8A} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs index 63816190cf..9a68105e33 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests public string Street { get; set; } } - [Fact] + [Fact(Skip = "Extra entries in model state #2446.")] public async Task FromBodyAndRequiredOnProperty_EmptyBody_AddsModelStateError() { // Arrange @@ -42,11 +42,18 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests ParameterType = typeof(Person) }; - var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); - var httpContext = operationContext.HttpContext; + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Id\":1234 }")); + request.ContentType = "application/json"; + }); - ConfigureHttpRequest(httpContext.Request, string.Empty); - var modelState = new ModelStateDictionary(); + var actionContext = operationContext + .HttpContext + .RequestServices + .GetRequiredService>().Value; + var modelState = actionContext.ModelState; // Act var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); @@ -79,10 +86,14 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests ParameterType = typeof(Person) }; - var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); - var httpContext = operationContext.HttpContext; + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{ \"Id\":1234 }")); + request.ContentType = "application/json"; + }); - ConfigureHttpRequest(httpContext.Request, "{ \"Id\":1234 }"); + var httpContext = operationContext.HttpContext; var modelState = new ModelStateDictionary(); // Act @@ -122,9 +133,14 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests ParameterType = typeof(Person4) }; - var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty)); + request.ContentType = "application/json"; + }); + var httpContext = operationContext.HttpContext; - ConfigureHttpRequest(httpContext.Request, string.Empty); var actionContext = httpContext.RequestServices.GetRequiredService>().Value; var modelState = actionContext.ModelState; @@ -177,9 +193,13 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests ParameterType = typeof(Person2) }; - var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(inputText)); + request.ContentType = "application/json"; + }); var httpContext = operationContext.HttpContext; - ConfigureHttpRequest(httpContext.Request, inputText); var modelState = new ModelStateDictionary(); // Act @@ -231,9 +251,13 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests ParameterType = typeof(Person3) }; - var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(inputText)); + request.ContentType = "application/json"; + }); var httpContext = operationContext.HttpContext; - ConfigureHttpRequest(httpContext.Request, inputText); var actionContext = httpContext.RequestServices.GetRequiredService>().Value; var modelState = actionContext.ModelState; @@ -258,11 +282,5 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests "Required property 'Zip' not found in JSON. Path ''", error.Exception.Message); } - - private static void ConfigureHttpRequest(HttpRequest request, string jsonContent) - { - request.Body = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); - request.ContentType = "application/json"; - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ByteArrayModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ByteArrayModelBinderIntegrationTest.cs new file mode 100644 index 0000000000..5b32f7c42d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ByteArrayModelBinderIntegrationTest.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNet.Mvc.IntegrationTests +{ + public class ByteArrayModelBinderIntegrationTest + { + private class Person + { + public byte[] Token { get; set; } + } + + [Theory(Skip = "Extra entries in model state #2446, ModelState.Value not set due to #2445, #2447")] + [InlineData(true)] + [InlineData(false)] + public async Task BindProperty_WithData_GetsBound(bool fallBackScenario) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Person) + }; + + var prefix = fallBackScenario ? string.Empty : "Parameter1"; + var queryStringKey = fallBackScenario ? "Token" : prefix + "." + "Token"; + + // any valid base64 string + var expectedValue = new byte[] { 12, 13 }; + var value = Convert.ToBase64String(expectedValue); + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.QueryString = new QueryString(queryStringKey, value); + }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Token); + Assert.Equal(expectedValue, boundPerson.Token); + + // ModelState + Assert.True(modelState.IsValid); + + Assert.Equal(2, modelState.Keys.Count); // Should be only two keys. bug #2446 + Assert.Single(modelState.Keys, k => k == prefix); + Assert.Single(modelState.Keys, k => k == queryStringKey); + + var key = Assert.Single(modelState.Keys, k => k == queryStringKey + "[0]"); + Assert.NotNull(modelState[key].Value); // should be non null bug #2445. + Assert.Empty(modelState[key].Errors); + Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); // Should be skipped. bug#2447 + + key = Assert.Single(modelState.Keys, k => k == queryStringKey + "[1]"); + Assert.NotNull(modelState[key].Value); // should be non null bug #2445. + Assert.Empty(modelState[key].Errors); + Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); // Should be skipped. bug#2447 + } + + [Fact] + public async Task BindParameter_NoData_DoesNotGetBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + + ParameterType = typeof(byte[]) + }; + + // No data is passed. + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(httpContext => { }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.Null(modelBindingResult); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Empty(modelState.Keys); + } + + [Fact(Skip = "ModelState.Value not set due to #2445, #2446")] + public async Task BindParameter_WithData_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + + ParameterType = typeof(byte[]) + }; + + // any valid base64 string + var value = "four"; + var expectedValue = Convert.FromBase64String(value); + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.QueryString = new QueryString("CustomParameter", value); + }); + + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + var model = Assert.IsType(modelBindingResult.Model); + + // Model + Assert.Equal(expectedValue, model); + + // ModelState + Assert.True(modelState.IsValid); + + Assert.Equal(3, modelState.Count); + Assert.Single(modelState.Keys, k => k == "CustomParameter[0]"); + Assert.Single(modelState.Keys, k => k == "CustomParameter[1]"); + var key = Assert.Single(modelState.Keys, k => k == "CustomParameter[2]"); + + Assert.NotNull(modelState[key].Value); + Assert.Equal(value, modelState[key].Value.AttemptedValue); + Assert.Equal(expectedValue, modelState[key].Value.RawValue); + Assert.Empty(modelState[key].Errors); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/CancellationTokenModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/CancellationTokenModelBinderIntegrationTest.cs new file mode 100644 index 0000000000..94f491557e --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/CancellationTokenModelBinderIntegrationTest.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNet.Mvc.IntegrationTests +{ + public class CancellationTokenModelBinderIntegrationTest + { + private class Person + { + public CancellationToken Token { get; set; } + } + + [Fact(Skip = "CancellationToken should not be validated #2447.")] + public async Task BindProperty_WithData__WithPrefix_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + + ParameterType = typeof(Person) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(httpContext => { }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Token); + + // ModelState + Assert.True(modelState.IsValid); + + Assert.Equal(2, modelState.Keys.Count); + Assert.Single(modelState.Keys, k => k == "CustomParameter"); + + var key = Assert.Single(modelState.Keys, k => k == "CustomParameter.Token"); + Assert.Null(modelState[key].Value); + Assert.Empty(modelState[key].Errors); + + // This Assert Fails. + Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState); + } + + [Fact(Skip = "CancellationToken should not be validated #2447,Extra entries in model state dictionary. #2466")] + public async Task BindProperty_WithData__WithEmptyPrefix_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Person) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(httpContext => { }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Token); + + // ModelState + Assert.True(modelState.IsValid); + var key = Assert.Single(modelState.Keys); + Assert.Equal("Token", key); + Assert.Null(modelState[key].Value); + Assert.Empty(modelState[key].Errors); + + // This Assert Fails. + Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState); + } + + [Fact(Skip = "CancellationToken should not be validated #2447.")] + public async Task BindParameter_WithData_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + + ParameterType = typeof(CancellationToken) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(httpContext => { }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var token = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(token); + + // ModelState + Assert.True(modelState.IsValid); + var key = Assert.Single(modelState.Keys); + Assert.Equal("CustomParameter", key); + Assert.Null(modelState[key].Value); + Assert.Empty(modelState[key].Errors); + + // This assert fails. + Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs new file mode 100644 index 0000000000..e1373b5b43 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Collections; +using Microsoft.AspNet.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNet.Mvc.IntegrationTests +{ + public class FormFileModelBindingIntegrationTest + { + private class Person + { + public Address Address { get; set; } + } + + private class Address + { + public int Zip { get; set; } + + public IFormFile File { get; set; } + } + + [Fact(Skip = "ModelState.Value not set due to #2445, Extra entries in model state #2446.")] + public async Task BindProperty_WithData_WithEmptyPrefix_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Person) + }; + + var data = "Some Data Is Better Than No Data."; + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + request.QueryString = new QueryString("Address.Zip", "12345"); + UpdateRequest(request, data, "Address.File"); + }); + + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson.Address); + var file = Assert.IsAssignableFrom(boundPerson.Address.File); + Assert.Equal("form-data; name=Address.File; filename=text.txt", file.ContentDisposition); + var reader = new StreamReader(boundPerson.Address.File.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Equal(2, modelState.Count); + Assert.Single(modelState.Keys, k => k == "Address.Zip"); + var key = Assert.Single(modelState.Keys, k => k == "Address.File"); // Should be only one key. bug #2446 + Assert.NotNull(modelState[key].Value); // should be non null bug #2445. + Assert.Empty(modelState[key].Errors); + Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); + } + + [Fact(Skip = "Extra entries in model state #2446.")] + public async Task BindParameter_WithData_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + // Setting a custom parameter prevents it from falling back to an empty prefix. + BinderModelName = "CustomParameter", + }, + + ParameterType = typeof(IFormFile) + }; + + var data = "Some Data Is Better Than No Data."; + var operationContext = ModelBindingTestHelper.GetOperationBindingContext( + request => + { + UpdateRequest(request, data, "CustomParameter"); + }); + + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var file = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(file); + Assert.Equal("form-data; name=CustomParameter; filename=text.txt", file.ContentDisposition); + var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + + // ModelState + Assert.True(modelState.IsValid); + + // Validation should be skipped because we do not validate any parameters and since IFormFile is not + // IValidatableObject, we should have no entries in the model state dictionary. + Assert.Empty(modelState.Keys); // Enable when we fix #2446. + } + + [Fact(Skip = "Extra entries in model state #2446.")] + public async Task BindParameter_NoData_DoesNotGetBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + + ParameterType = typeof(IFormFile) + }; + + // No data is passed. + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => + { + request.ContentType = "multipart/form-data"; + }); + + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); // Fails due to bug #2456 + Assert.Null(modelBindingResult.Model); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Empty(modelState.Keys); + } + + private void UpdateRequest(HttpRequest request, string data, string name) + { + var fileCollection = new FormFileCollection(); + var formCollection = new FormCollection(new Dictionary(), fileCollection); + var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(data)); + + request.Form = formCollection; + request.ContentType = "multipart/form-data"; + request.Headers["Content-Disposition"] = "form-data; name=" + name + "; filename=text.txt"; + fileCollection.Add(new FormFile(memoryStream, 0, data.Length) + { + Headers = request.Headers + }); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/HeaderModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/HeaderModelBinderIntegrationTest.cs new file mode 100644 index 0000000000..0ed9f8b425 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/HeaderModelBinderIntegrationTest.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNet.Mvc.IntegrationTests +{ + public class HeaderModelBinderIntegrationTest + { + private class Person + { + public Address Address { get; set; } + } + + private class Address + { + [FromHeader(Name = "Header")] + [Required] + public string Street { get; set; } + } + + [Fact] + public async Task BindPropertyFromHeader_NoData_UsesFullPathAsKeyForModelStateErrors() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + ParameterType = typeof(Person) + }; + + // Do not add any headers. + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + + // ModelState + Assert.False(modelState.IsValid); + var key = Assert.Single(modelState.Keys); + Assert.Equal("CustomParameter.Address.Header", key); + var error = Assert.Single(modelState[key].Errors); + Assert.Equal("The Street field is required.", error.ErrorMessage); + } + + [Fact(Skip = "ModelState.Value not set due to #2445")] + public async Task BindPropertyFromHeader_WithPrefix_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "prefix", + }, + ParameterType = typeof(Person) + }; + + // Do not add any headers. + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { + request.Headers.Add("Header", new[] { "someValue" }); + }); + + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Address); + Assert.Equal("someValue", boundPerson.Address.Street); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Equal(3, modelState.Count); + Assert.Single(modelState.Keys, k => k == "prefix.Address"); + Assert.Single(modelState.Keys, k => k == "prefix"); + + var key = Assert.Single(modelState.Keys, k => k == "prefix.Address.Header"); + Assert.NotNull(modelState[key].Value); + Assert.Equal("someValue", modelState[key].Value.RawValue); + Assert.Equal("someValue", modelState[key].Value.AttemptedValue); + } + + // The scenario is interesting as we to bind the top level model we fallback to empty prefix, + // and hence the model state keys have an empty prefix. + [Fact(Skip = "ModelState.Value not set due to #2445. ModelState should not have empty key #2466.")] + public async Task BindPropertyFromHeader_WithData_WithEmptyPrefix_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Person) + }; + + // Do not add any headers. + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(request => { + request.Headers.Add("Header", new[] { "someValue" }); + }); + + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Address); + Assert.Equal("someValue", boundPerson.Address.Street); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Equal(2, modelState.Count); + Assert.Single(modelState.Keys, k => k == "Address"); + var key = Assert.Single(modelState.Keys, k => k == "Address.Header"); + Assert.NotNull(modelState[key].Value); + Assert.Equal("someValue", modelState[key].Value.RawValue); + Assert.Equal("someValue", modelState[key].Value.AttemptedValue); + } + + [Theory(Skip = "Extra entries in model state #2446.")] + [InlineData(typeof(string[]), "value1, value2, value3")] + [InlineData(typeof(string), "value")] + public async Task BindParameterFromHeader_WithData_WithPrefix_ModelGetsBound(Type modelType, string value) + { + // Arrange + var expectedValue = value.Split(',').Select(v => v.Trim()); + + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + BindingSource = BindingSource.Header + }, + ParameterType = modelType + }; + + Action action = (r) => r.Headers.Add("CustomParameter", new[] { value }); + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(action); + + // Do not add any headers. + var httpContext = operationContext.HttpContext; + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + Assert.NotNull(modelBindingResult.Model); + Assert.IsType(modelType, modelBindingResult.Model); + + // ModelState + Assert.True(modelState.IsValid); + var key = Assert.Single(modelState.Keys); + Assert.Equal("CustomParameter", key); + + Assert.NotNull(modelState[key].Value); + Assert.Equal(expectedValue, modelState[key].Value.RawValue); + Assert.Equal(value, modelState[key].Value.AttemptedValue); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs index 11da5127e3..27eca799d7 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding.Validation; @@ -11,19 +12,19 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests { public static class ModelBindingTestHelper { - public static OperationBindingContext GetOperationBindingContext() + public static OperationBindingContext GetOperationBindingContext(Action updateRequest) { - var httpContext = ModelBindingTestHelper.GetHttpContext(); - var actionBindingContextAccessor = + var httpContext = ModelBindingTestHelper.GetHttpContext(updateRequest); + var actionBindingContext = httpContext.RequestServices.GetRequiredService>().Value; return new OperationBindingContext() { BodyBindingState = BodyBindingState.NotBodyBased, HttpContext = httpContext, MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), - ValidatorProvider = actionBindingContextAccessor.ValidatorProvider, - ValueProvider = actionBindingContextAccessor.ValueProvider, - ModelBinder = actionBindingContextAccessor.ModelBinder + ValidatorProvider = actionBindingContext.ValidatorProvider, + ValueProvider = actionBindingContext.ValueProvider, + ModelBinder = actionBindingContext.ModelBinder }; } @@ -39,10 +40,13 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests metadataProvider)); } - public static HttpContext GetHttpContext() + public static HttpContext GetHttpContext(Action updateRequest) { var options = (new TestMvcOptions()).Options; var httpContext = new DefaultHttpContext(); + + updateRequest(httpContext.Request); + var serviceCollection = MvcServices.GetDefaultServices(); httpContext.RequestServices = serviceCollection.BuildServiceProvider(); diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs new file mode 100644 index 0000000000..c410111bed --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Xunit; + +namespace Microsoft.AspNet.Mvc.IntegrationTests +{ + public class ServicesModelBinderIntegrationTest + { + private class Person + { + public Address Address { get; set; } + } + + private class Address + { + // Using a service type already in defaults. + [FromServices] + public JsonOutputFormatter OutputFormatter { get; set; } + } + + [Fact] + public async Task BindPropertyFromService_WithData_WithPrefix_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + + ParameterType = typeof(Person) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(httpContext => { }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Address.OutputFormatter); + + // ModelState + Assert.True(modelState.IsValid); + + Assert.Equal(3, modelState.Keys.Count); + Assert.Single(modelState.Keys, k => k == "CustomParameter"); + Assert.Single(modelState.Keys, k => k == "CustomParameter.Address"); + + var key = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.OutputFormatter"); + Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState); + Assert.Null(modelState[key].Value); + Assert.Empty(modelState[key].Errors); + } + + [Fact(Skip = "ModelState should not have empty key #2466.")] + public async Task BindPropertyFromService_WithData_WithEmptyPrefix_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Person) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(httpContext => { }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Address.OutputFormatter); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Equal(2, modelState.Keys.Count); + Assert.Single(modelState.Keys, k => k == "Address"); + var key = Assert.Single(modelState.Keys, k => k == "Address.OutputFormatter"); + Assert.Equal(ModelValidationState.Skipped, modelState[key].ValidationState); + Assert.Null(modelState[key].Value); // For non user bound models there should be no value. + Assert.Empty(modelState[key].Errors); + } + + [Fact(Skip = "#2464 ModelState should not have entry for non request bound models.")] + public async Task BindParameterFromService_WithData_GetsBound() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + BindingSource = BindingSource.Services + }, + + // Using a service type already in defaults. + ParameterType = typeof(JsonOutputFormatter) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(httpContext => { }); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + + // ModelBindingResult + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + + // Model + var outputFormatter = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(outputFormatter); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Empty(modelState.Keys); + } + } +} \ No newline at end of file