How can I test Controller.ViewData.ModelState? I would prefer to do it without any mock framework.
You don't have to use a Mock if you're using the Repository Pattern for your data, of course.
Some examples:
http://www.singingeels.com/Articles/Test_Driven_Development_with_ASPNET_MVC.aspx
// Test for required "FirstName".
controller.ViewData.ModelState.Clear();
newCustomer = new Customer
{
FirstName = "",
LastName = "Smith",
Zip = "34275",
};
controller.Create(newCustomer);
// Make sure that our validation found the error!
Assert.IsTrue(controller.ViewData.ModelState.Count == 1,
"FirstName must be required.");
//[Required]
//public string Name { get; set; }
//[Required]
//public string Description { get; set; }
ProductModelEdit model = new ProductModelEdit() ;
//Init ModelState
var modelBinder = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
() => model, model.GetType()),
ValueProvider=new NameValueCollectionValueProvider(
new NameValueCollection(), CultureInfo.InvariantCulture)
};
var binder=new DefaultModelBinder().BindModel(
new ControllerContext(),modelBinder );
ProductController.ModelState.Clear();
ProductController.ModelState.Merge(modelBinder.ModelState);
ViewResult result = (ViewResult)ProductController.CreateProduct(null,model);
Assert.IsTrue(result.ViewData.ModelState["Name"].Errors.Count > 0);
Assert.True(result.ViewData.ModelState["Description"].Errors.Count > 0);
Assert.True(!result.ViewData.ModelState.IsValid);
For testing Web API, use the Validate method on the controller:
var controller = new MyController();
controller.Configuration = new HttpConfiguration();
var model = new MyModel();
controller.Validate(model);
var result = controller.MyMethod(model);
Ran into this problem for .NetCore 2.1
Here's my solution:
Extension Method
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace MyExtension
{
public static void BindViewModel<T>(this Controller controller, T model)
{
if (model == null) return;
var context = new ValidationContext(model, null, null);
var results = new List<ValidationResult>();
if (!Validator.TryValidateObject(model, context, results, true))
{
controller.ModelState.Clear();
foreach (ValidationResult result in results)
{
var key = result.MemberNames.FirstOrDefault() ?? "";
controller.ModelState.AddModelError(key, result.ErrorMessage);
}
}
}
}
View Model
public class MyViewModel
{
[Required]
public string Name { get; set; }
}
Unit Test
public async void MyUnitTest()
{
// helper method to create instance of the Controller
var controller = this.CreateController();
var model = new MyViewModel
{
Name = null
};
// here we call the extension method to validate the model
// and set the errors to the Controller's ModelState
controller.BindViewModel(model);
var result = await controller.ActionName(model);
Assert.NotNull(result);
var viewResult = Assert.IsType<BadRequestObjectResult>(result);
}
This not only let's you check that the error exists but also checks that it has the exact same error message as expected. For example both of these parameters are Required so their error message shows as "Required".
Model markup:
//[Required]
//public string Name { get; set; }
//[Required]
//public string Description { get; set; }
Unit test code:
ProductModelEdit model = new ProductModelEdit() ;
//Init ModelState
var modelBinder = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
() => model, model.GetType()),
ValueProvider=new NameValueCollectionValueProvider(
new NameValueCollection(), CultureInfo.InvariantCulture)
};
var binder=new DefaultModelBinder().BindModel(
new ControllerContext(),modelBinder );
ProductController.ModelState.Clear();
ProductController.ModelState.Merge(modelBinder.ModelState);
ViewResult result = (ViewResult)ProductController.CreateProduct(null,model);
Assert.IsTrue(!result.ViewData.ModelState.IsValid);
//Make sure Name has correct errors
Assert.IsTrue(result.ViewData.ModelState["Name"].Errors.Count > 0);
Assert.AreEqual(result.ViewData.ModelState["Name"].Errors[0].ErrorMessage, "Required");
//Make sure Description has correct errors
Assert.IsTrue(result.ViewData.ModelState["Description"].Errors.Count > 0);
Assert.AreEqual(result.ViewData.ModelState["Description"].Errors[0].ErrorMessage, "Required");
Adding to the great answers above, check out this fantastic use of the protected TryValidateModel method within the Controller class.
Simply create a test class inheriting from controller and pass your model to the TryValidateModel method. Here's the link:
http://blog.icanmakethiswork.io/2013/03/unit-testing-modelstate.html
Full credit goes to John Reilly and Marc Talary for this solution.
Related
I am very beginner in this C# world.
There is this model class:
public class Fund
{
[Required]
public int id { get; set; }
[Required]
public string name { get; set; }
[Required]
public string nickname { get; set; }
}
Although the [Required] annotation is over all properties, an instance like:
Fund f = new Fund();
f.name = "test name";
f.nickname = "test ninckname";
always pass in a test like:
if (ModelState.IsValid)
{
// do stuff
}
How am I supposed to set the model such that an instance like that won't pass in in the ModelState.IsValid test?
Other instances like:
Fund f1 = new Fund();
f1.id = 3;
f1.nickname = "test ninckname";
and
Fund f2 = new Fund();
f2.id = 3;
f2.name = "test name";
are also passing on the test.
EDIT:
The ModelState.IsValid is inside a controller, I am testing the controller actually.
EDIT 2:
That is the controller's method signature:
[HttpPatch]
public ActionResult EditFund(Fund fund)
EDIT 3:
That is my whole test method:
[TestMethod]
public void TestEditInvalidFund()
{
// Arrange
FundController controller = new FundController();
controller.ControllerContext = TestModelHelper.AdminControllerContext();
var _fund = new Mock<IFund>();
_fund.SetupGet(f => f.id).Returns(1);
//_fund.SetupGet(f => f.name).Returns("Fund name");
_fund.SetupGet(f => f.nickname).Returns("Fund nickname");
_fund.Setup(f => f.Edit()).Callback(() => {}).Verifiable();
// Act
var result = (JsonResult)controller.EditFund(_fund.Object);
// Assert
SimpleMessage resultMessage = m_serializer.Deserialize<SimpleMessage>(m_serializer.Serialize(result.Data));
Assert.IsNotNull(resultMessage.status, "JSON record does not contain 'status' required property.");
Assert.IsTrue(resultMessage.status.Equals("fail"), "status must be 'fail'");
}
And that is the whole controller's method:
[HttpPatch]
public ActionResult EditFund(IFund _fund)
{
try
{
if (ModelState.IsValid)
{
//_fund.Edit();
}
else
{
string error_messages = "";
foreach (var e in ModelState.Select(x => x.Value.Errors).Where(y => y.Count > 0).ToList())
{
error_messages += e[0].ErrorMessage + "\n";
}
throw new Exception(error_messages);
}
return MessageHelper(#Resources.Global.success, #Resources.Funds.success_editing_fund, "success");
}
catch (Exception err)
{
return ErrorHelper(#Resources.Global.error, #Resources.Funds.error_editing_fund, err.Message);
}
}
You can't unit test ModelState.IsValid. Well, you can, but you need extra code to do it, and it's not exactly ideal.
Here's my code on github: WithModelStateIsInvalid<TController>()
And here's the NuGet package it's in: https://www.nuget.org/packages/TestBase-Mvc/
And here's how I use it:
UnitUnderTest
.WithModelStateIsInValid()
.Action(model)
.ShouldBeViewResult()
.ShouldBeAnInvalidModel();
—————————————————————————————————————————
The reason you can't unit test it, is that Model Validation happens in the ModelBinding step before your controller action is called.
So to simulate a model validation failure, the easiest way is to invalidate controller.ModelState 'manually' in your unit test code, before calling the action. Which is what the extension method does, but actually it's just one-line:
controller.ModelState.AddModelError("FieldName", #"error message")
(A more sophisticated extension method would probably let you specify which Model key is invalid. PRs always welcome).
[Required]
public int id { get; set; }
This will always be valid since an int is not nullable. It has a default value of 0.
Not sure, why it gets valid, but you can try to explicitly revalidate the model by:
TryValidateModel(fund)
I have an ApiController, in which I have a Post method that accepts a VariableTemplateViewModel, which looks like this:
public class VariableTemplateViewModel
{
public VariableTemplateViewModel() { }
public double Version { get; set; }
[Required]
public List<VariableViewModel> Variables { get; set; }
}
public class VariableViewModel
{
public VariableViewModel() { }
[Required(AllowEmptyStrings=false, ErrorMessage="Variable Name cannot be empty")]
public string Name { get; set; }
}
Inside the Post method, I do a validation check, like so:
public void Post(long id, [FromBody]VariableTemplateViewModel model)
{
if (!ModelState.IsValid )
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
}
This works great and if I call it with a view model that has empty Name fields in the Variables list, it fails validation. However, when I try to validate this from the Unit Test, it only runs validations on VariableViewModel itself and not recursively the VariableViewModel. So, if I pass in null for the Variables, I get a validation error but if I pass in an empty string for Name, there are no validation errors.
[TestMethod]
public void Post_Returns_HttpResponseException_With_Empty_Variable_Name()
{
var controller = new VariableController();
var viewModel = new VariableTemplateViewModel
{
Version = 1,
Variables = new List<VariableViewModel>
{
new VariableViewModel { Name = "" }
}
};
var validationContext = new ValidationContext(viewModel, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(viewModel, validationContext, validationResults, true);
foreach (var validationResult in validationResults)
{
controller.ModelState.AddModelError(validationResult.MemberNames.First(), validationResult.ErrorMessage);
}
// Assert
}
I have tried removing/adding empty constructors and initializing Variables inside the VariableTemplateViewModel constructor. I have even tried using Validator.TryValidateObject on viewModel.Variables directly to no avail.
Does anyone know how this can be fixed?
I have a same problem and use this for the solution. May this help You.
public void Post(long id, [FromBody]VariableTemplateViewModel model)
{
if (!ModelState.IsValid && TryValidateModel(model.NestedModel, "NestedModel."))
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
}
Try this, Recursive Validation Using DataAnnotations
In my MVC Controller, I have this code (after adding an object, I redirect user to edit that object):
[PrivilegeRequirement("DB_ADMIN")]
public ActionResult Add()
{
MongoDataContext dc = new MongoDataContext();
var model = new ModelObj();
dc.Models.Insert(model);
return this.RedirectToAction("Edit", new { pId = model.Id, });
}
[PrivilegeRequirement("DB_ADMIN")]
public ActionResult Edit(ObjectId? pId)
{
MongoDataContext dc = new MongoDataContext();
var model = dc.Models.FindOneById(pId);
if (model == null)
{
Session["error"] = "No model with this ID found.";
return this.RedirectToAction("");
}
return this.View(model);
}
However, pId is always null, making the FindOneById always return null. I have debugged and made sure that the Id had value when passing from Add Action. Moreover, I tried adding a testing parameter:
[PrivilegeRequirement("DB_ADMIN")]
public ActionResult Add()
{
MongoDataContext dc = new MongoDataContext();
var model = new ModelObj();
dc.Models.Insert(model);
return this.RedirectToAction("Edit", new { pId = model.Id, test = 10 });
}
[PrivilegeRequirement("DB_ADMIN")]
public ActionResult Edit(ObjectId? pId, int? test)
{
MongoDataContext dc = new MongoDataContext();
var model = dc.Models.FindOneById(pId);
if (model == null)
{
Session["error"] = "No model with this ID found.";
return this.RedirectToAction("");
}
return this.View(model);
}
When I debug, I received the test parameter in Edit Action with value of 10 correctly, but pId is null. Please tell me what I did wrong, and how to solve this problem?
I would suspect that the ObjectId is not serializing/deserializing correctly. Given that it doesn't make for a great WebAPI anyway, I generally use a string and convert within the method to an ObjectId via the Parse method (or use TryParse):
public ActionResult Edit(string id, int? test)
{
// might need some error handling here .... :)
var oId = ObjectId.Parse(id);
}
You can use ToString on the ObjectId to convert it to a string for calling:
var pId = model.Id.ToString();
(I realize this question is very similar to How to whitelist/blacklist child object fields in the ModelBinder/UpdateModel method? but my situation is slightly different and there may be a better solution available now that wasn't then.)
Our company sells web-based software that is extremely configurable by the end-user. The nature of this flexibility means that we must do a number of things at run time that would normally be done at compile time.
There are some rather complex rules regarding who has read or read/write access to most everything.
For instance, take this model that we would like to create:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace j6.Business.Site.Models
{
public class ModelBindModel
{
[Required]
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string FirstName { get; set; }
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string MiddleName { get; set; }
[Required]
[Whitelist(ReadAccess = true, WriteAccess = true)]
public string LastName { get; set; }
[Required]
[Whitelist(ReadAccess = User.CanReadSalary, WriteAccess = User.CanWriteSalary)]
public string Salary { get; set; }
[Required]
[Whitelist(ReadAccess = User.CanReadSsn, WriteAccess = User.CanWriteSsn)]
public string Ssn { get; set; }
[Required]
public string SirNotAppearingOnThisPage { get; set; }
}
}
In the controller, it is not difficult to "unbind" things manually.
var resetValue = null;
modelState.Remove(field);
pi = model.GetType().GetProperty(field);
if (pi == null)
{
throw new Exception("An exception occured in ModelHelper.RemoveUnwanted. Field " +
field +
" does not exist in the model " + model.GetType().FullName);
}
// Set the default value.
pi.SetValue(model, resetValue, null);
Using HTML helpers, I can easily access the model metadata and suppress rendering of any fields the user does not have access to.
The kicker: I can't figure out how to access the model metadata anywhere in the CONTROLLER itself to prevent over-posting.
Note that using [Bind(Include...)] is not a functional solution, at least not without additional support. The properties to Include are run-time (not compile time) dependent, and excluding the property does not remove it from the validation.
ViewData.Model is null
ViewData.ModelMetaData is null
[AllowAnonymous]
[HttpPost]
// [Bind(Exclude = "Dummy1" + ",Dummy2")]
public ViewResult Index(ModelBindModel dto)
{
zzz.ModelHelper.RemoveUnwanted(ModelState, dto, new string[] {"Salary", "Ssn"});
ViewBag.Method = "Post";
if (!ModelState.IsValid)
{
return View(dto);
}
return View(dto);
}
Any suggestions on how to access the Model MetaData from the controller? Or a better way to whitelist properties at run time?
Update:
I borrowed a page from this rather excellent resource:
http://www.dotnetcurry.com/ShowArticle.aspx?ID=687
With a model that looks like this:
[Required]
[WhiteList(ReadAccessRule = "Nope", WriteAccessRule = "Nope")]
public string FirstName { get; set; }
[Required]
[WhiteList(ReadAccessRule = "Database.CanRead.Key", WriteAccessRule = "Database.CanWrite.Key")]
public string LastName { get; set; }
The class:
public class WhiteList : Attribute
{
public string ReadAccessRule { get; set; }
public string WriteAccessRule { get; set; }
public Dictionary<string, object> OptionalAttributes()
{
var options = new Dictionary<string, object>();
var canRead = false;
if (ReadAccessRule != "")
{
options.Add("readaccessrule", ReadAccessRule);
}
if (WriteAccessRule != "")
{
options.Add("writeaccessrule", WriteAccessRule);
}
if (ReadAccessRule == "Database.CanRead.Key")
{
canRead = true;
}
options.Add("canread", canRead);
options.Add("always", "be there");
return options;
}
}
And adding these lines to the MetadataProvider class mentioned in the link:
var whiteListValues = attributes.OfType<WhiteList>().FirstOrDefault();
if (whiteListValues != null)
{
metadata.AdditionalValues.Add("WhiteList", whiteListValues.OptionalAttributes());
}
Finally, the heart of the system:
public static void DemandFieldAuthorization<T>(ModelStateDictionary modelState, T model)
{
var metaData = ModelMetadataProviders
.Current
.GetMetadataForType(null, model.GetType());
var props = model.GetType().GetProperties();
foreach (var p in metaData.Properties)
{
if (p.AdditionalValues.ContainsKey("WhiteList"))
{
var whiteListDictionary = (Dictionary<string, object>) p.AdditionalValues["WhiteList"];
var key = "canread";
if (whiteListDictionary.ContainsKey(key))
{
var value = (bool) whiteListDictionary[key];
if (!value)
{
RemoveUnwanted(modelState, model, p.PropertyName);
}
}
}
}
}
To recap my interpretation of your question:
Field access is dynamic; some users may be able to write to a field and some may not.
You have a solution to control this in the view.
You want to prevent a malicious form submission from sending restricted properties, which the model binder will then assign to your model.
Perhaps something like this?
// control general access to the method with attributes
[HttpPost, SomeOtherAttributes]
public ViewResult Edit( Foo model ){
// presumably, you must know the user to apply permissions?
DemandFieldAuthorization( model, user );
// if the prior call didn't throw, continue as usual
if (!ModelState.IsValid){
return View(dto);
}
return View(dto);
}
private void DemandFieldAuthorization<T>( T model, User user ){
// read the model's property metadata
// check the user's permissions
// check the actual POST message
// throw if unauthorized
}
I wrote an extension method a year or so ago that has stood me in good stead a couple of times since. I hope this is of some help, despite not being perhaps the full solution for you. It essentially only allows validation on the fields that have been present on the form sent to the controller:
internal static void ValidateOnlyIncomingFields(this ModelStateDictionary modelStateDictionary, FormCollection formCollection)
{
IEnumerable<string> keysWithNoIncomingValue = null;
IValueProvider valueProvider = null;
try
{
// Transform into a value provider for linq/iteration.
valueProvider = formCollection.ToValueProvider();
// Get all validation keys from the model that haven't just been on screen...
keysWithNoIncomingValue = modelStateDictionary.Keys.Where(keyString => !valueProvider.ContainsPrefix(keyString));
// ...and clear them.
foreach (string errorKey in keysWithNoIncomingValue)
modelStateDictionary[errorKey].Errors.Clear();
}
catch (Exception exception)
{
Functions.LogError(exception);
}
}
Usage:
ModelState.ValidateOnlyIncomingFields(formCollection);
And you'll need a FormCollection parameter on your ActionResult declaration, of course:
public ActionResult MyAction (FormCollection formCollection) {
I'm getting my feet wet with the Entity Framework and am wondering if there is a way for me to check model state without model binding happening.
Say I create a user primarily from code, is there a way for me to check to make sure it is valid according to my predefined data annotations before I update?
public ActionResult Index()
{
User u = new User();
u.Username = "test";
u.Password = "test";
u.Email = "test";
DefaultContext db = new DefaultContext();
if (ModelState.IsValid)
{
db.Users.Add(u);
db.SaveChanges();
Response.Write(u.Id);
}
else
// model is not valid
return View();
}
The above code does not work because there is no binding happening. Or maybe I am confused of the process.
Thanks.
ModelState won't be available because it's set up by the model binder. The ModelBinder didn't bind a model so it didn't run validation thus ModelState will be empty.
But that doesn't stop you from using attributes and performing validation.
Assuming you are using the DataAnnotation attributes, you can run the validation without model binding.
Given this class:
public class Widget
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
public decimal Price { get; set; }
}
You can do this:
var widget = new Widget
{
Id = 12,
Price = 15.57M
};
var context = new ValidationContext(widget, null, null);
var results = new List<ValidationResult>();
if( Validator.TryValidateObject( widget, context, results, true ) )
{
//Validation Successful
}
else
{
//Validation Failed
}
The Name property was not set and the TryValidateObject() will fail. The results collection will have the actual error that occurred.
you can use TryValidateModel or ValidateModel
User u = new User();
u.Username = "test";
u.Password = "test";
u.Email = "test";
if (TryValidateModel(u))
{
}else{
}