C# MVC Model is always valid when Unit Testing a Controller Action - c#

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)

Related

Selectively Whitelisting Model Fields to Bind

(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) {

Trying to pass a parameter but getting a "context" error

I am trying to pass this from my controller into my view (#ViewBag.Chapter7Total):
ViewBag.Chapter7Total = calc.CalculatePrice(quoteData, Chapter7);
But am getting a "doesn't exist in the current context error" in VS.
Basically, I am trying to pass in a second parameter which determines which pricing structure to use between 2. Chapter7 or Chapter13, with the selection determining the second parameter to perform calculations with.
Here are my methods:
class Chapter
{
public decimal PaymentPlan { get; set; }
public decimal Price { get; set; }
}
public decimal decPaymentPlan(QuoteData quoteData, Chapter chapter)
{
if (quoteData.StepFilingInformation.PaymentPlanRadioButton
== StepFilingInformation.PaymentPlan.No)
return PriceQuote.priceNoPaymentPlan;
else
return chapter.PaymentPlan;
}
public decimal Calculate(QuoteData quoteData, Chapter chapter)
{
decimal total = chapter.Price;
total += this.decPaymentPlan(quoteData, chapter);
return total;
}
static Chapter Chapter7 = new Chapter() { Price = 799.00m, PaymentPlan = 100.00m };
Finally, this is my controller:
public ActionResult EMailQuote()
{
Calculations calc = new Calculations();
Chapter chap = new Chapter();
QuoteData quoteData = new QuoteData
{
StepFilingInformation = new Models.StepFilingInformation
{
//just moking user input here temporarily to test out the UI
PaymentPlanRadioButton = Models.StepFilingInformation.PaymentPlan.Yes,
}
};
var total = calc.CalculatePrice(quoteData);
ViewBag.Chapter7Total = calc.CalculatePrice(quoteData, Chapter7);
return View(quoteData);
}
I'm not sure what to do to pass Chapter7. Any thoughts?
UPDATE 1:
This is my ViewModel (QuoteData):
public class QuoteData
{
public PriceQuote priceQuote;
public Calculations calculations;
public StepFilingInformation stepFilingInformation { get; set; }
public QuoteData()
{
PriceQuote = new PriceQuote();
Calculations = new Calculations();
}
}
I'm trying to figure out what you are doing here but I see that most importantly, you are sending quoteData to your View. I'm making a guess here but I figure QuoteData is a custom entity type of yours and not a ViewModel.
To start, I would create a QuoteDataViewModel in your models with all the properties of QuoteData that you need, including
public class QuoteDataViewModel {
... all of your quoteData properties here
public Chapter Chapter7 { get; set; }
}
In your EMailQuote action, something similar to this
public ActionResult EMailQuote() {
...
var model = new QuoteDataViewModel();
var quoteData = new QuoteData();
... // map your quoteData to your model with Automapper or manually like
... // model.SomeProperty = quoteData.SomeProperty;
... // repeat for all properties
model.Chapter7 = Chapter7;
return View(model);
}
If you are posting this data back you would need your Post action to accept the new QuoteDataViewModel
public ActionResult EmailQuote(QuoteDataViewModel model) {
if(ModelState.IsValid) {
....//save data that was entered?
}
return View(model);
}
Your view would then take a QuoteDateViewModel
#model QuoteDataViewModel
This is all just how I would do it personally, I don't quite understand what you have going on, for example, this line:
var total = calc.CalculatePrice(quoteData);
I don't see total ever being used after you create it.
Anyway, that's just a sample of how I'd do it, create a model specific to the view, include any and all properties I need, populate the model in the controller and send it to the view
Update
Based on the OP comment that quoteData is a ViewModel, then just as above, adding the new property to hold the extra data is simple, by adding ...
public decimal QuoteTotal { get; set; }
public Chapter Chapter7 { get; set; }
...to the ViewModel
the controller populates
var total = calc.CalculatePrice(quoteData);
model.QuoteTotal = total;
model.Chapter7 = new Chapter();
model.Chapter7 = Chapter7;
In the View the values can be accessed like:
#Html.DisplayFor(model => model.QuoteTotal)
#Html.DisplayFor(model => model.Chapter7.PaymentPlan)
#Html.DisplayFor(model => model.Chapter7.Price)

ASP.NET MVC TryValidateModel() Issues when Model is Modified

I have a two step form process where the first set of data is stored in session.
[IsMp4File]
[Required(ErrorMessage = "* Please select a video to upload")]
public HttpPostedFileBase VideoClip { get; set; }
[Required(ErrorMessage = "* Please select a thumbmail image")]
public HttpPostedFileBase VideoThumbnail{ get; set; }
public string VideoFileName { get { return VideoClip.FileName; } }
public NewsWizardStep CurrentStep { get; set; }
...
public enum NewsWizardStep : int
{
One = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6
}
Controller
public ActionResult TvCreate(TvNewsVideoVM modelVM)
{
if (modelVM.CurrentStep == NewsWizardStep.Two)
{
var sessionModel = ((TvNewsVideoVM)Session["TvModelVM"]);
modelVM.VideoClip = sessionModel.VideoClip;
modelVM.VideoThumbnail = sessionModel.VideoThumbnail;
}
if (TryValidateModel(modelVM))
{
...
}
}
TryValidateModel(modelVM) returns false, saying VideoClip and VideoThumnail are required, despite mapping them from the seesionModel to the viewModel. I have added a breakpoint and checked they are not null.
It looks like there is some underlying functionality I am not aware of regarding how ModelState and ValidateModel() work , I just don't know what.
UPDATE
I wouldn't say I have resolved the issue but figured out a workaround that isn't that pretty, By going into the ModelState it is possible to set the ModelValue using SetModelValue() then manually remove the error from the model state and then call TryValidateModel() - you might not even have to add the values just remove the error I have not tried. Here is my work around.
if (modelVM.CurrentStep == NewsWizardStep.Two)
{
var sessionModel = ((MtTvNewsVideoVM)Session["MtTvModelVM"]);
modelVM.VideoClip = sessionModel.VideoClip;
modelVM.VideoThumbnail = sessionModel.VideoThumbnail;
ModelState.SetModelValue("VideoClip", new ValueProviderResult(sessionModel.VideoThumbnail, sessionModel.VideoFileName, CultureInfo.CurrentCulture));
ModelState.SetModelValue("VideoThumbnail", new ValueProviderResult(sessionModel.VideoClip, sessionModel.VideoFileName, CultureInfo.CurrentCulture));
ModelState["VideoClip"].Errors.RemoveAt(0);
ModelState["VideoThumbnail"].Errors.RemoveAt(0);
}
During the model binding the DefaultModelBinder validates your action parameters.
So when the execution hits your public ActionResult TvCreate(TvNewsVideoVM modelVM) method
the ModelState is already containing the validation errors.
When you call TryValidateModel it doesn't clear the ModelState so the validation errors remain there that is why it returns false. So you need to clear the ModelState collection if you want to redo the validation later manually:
public ActionResult TvCreate(TvNewsVideoVM modelVM)
{
ModelState.Clear();
if (modelVM.CurrentStep == NewsWizardStep.Two)
{
var sessionModel = ((TvNewsVideoVM)Session["TvModelVM"]);
modelVM.VideoClip = sessionModel.VideoClip;
modelVM.VideoThumbnail = sessionModel.VideoThumbnail;
}
if (TryValidateModel(modelVM))
{
...
}
}

Can I check modelstate without modelbinding?

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{
}

How can I test ModelState?

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.

Categories