We want to test two behaviors of the Delete action method. The first is that when a valid ProductID is passed as a parameter, the action method calls the DeleteProduct method of the repository and passes the correct Product object to be deleted. Here is the test:
[TestMethod]
public void Can_Delete_Valid_Products() {
// Arrange - create a Product
Product prod = new Product { ProductID = 2, Name = "Test" };
// Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1"}, prod,
new Product {ProductID = 3, Name = "P3"}, }.AsQueryable());
// Arrange - create the controller
AdminController target = new AdminController(mock.Object);
// Act - delete the product target.Delete(prod.ProductID);
// Assert - ensure that the repository delete method was // called with the correct Product
mock.Verify(m => m.DeleteProduct(prod));
}
The second test is to ensure that if the parameter value passed to the Delete method does not correspond to a valid product in the repository, the repository DeleteProduct method is not called.
Here is the test:
[TestMethod]
public void Cannot_Delete_Invalid_Products() { // Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] { new Product {ProductID = 1, Name = "P1"}, new Product {ProductID = 2, Name = "P2"}, new Product {ProductID = 3, Name = "P3"}, }.AsQueryable());
// Arrange - create the controller
AdminController target = new AdminController(mock.Object);
// Act - delete using an ID that doesn't exist target.Delete(100);
// Assert - ensure that the repository delete method was // called with the correct Product
mock.Verify(m => m.DeleteProduct(It.IsAny<Product>()), Times.Never());
}
You can see the new function at work simply by clicking one of the Delete buttons in the product list page, as shown in Figure 9-15. As shown in the figure, we have taken advantage of the TempData variable to display a message when a product is deleted from the catalog.
Figure 9-15. Deleting a product from the catalog
And at this point, we’ve implemented all of the CRUD operations. We can now create, read, update, and delete products.
Securing the Administration Features
It won’t have escaped your attention that anyone would be able to modify the product catalog if we deployed the application right now. All someone would need to know is that the administration features are available using the Admin/Index URL. To prevent random people from wreaking havoc, we are going to password-protect access to the entire Admin controller.
Setting Up Forms Authentication
Since ASP.NET MVC is built on the core ASP.NET platform, we have access to the ASP.NET Forms Authentication facility, which is a general-purpose system for keeping track of who is logged in. We’ll cover forms authentication in more detail in Chapter 22. For now, we’ll simply show you how to set up the most basic of configurations.
If you open the Web.config file, you will be able to find a section entitled authentication, like this one:
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880"/>
</authentication>
As you can see, forms authentication is enabled automatically in an MVC application created with the Empty or Internet Application template. The loginUrl attribute tells ASP.NET which URL users should be directed to when they need to authenticate themselves—in this case, the /Account/Logon page. The timeout attribute specifies how long a user is authenticated after logging in. By default, this is 48 hours (2,880 minutes). We’ll explain some of the other configuration options in Chapter 22.
Note The main alternative to forms authentication is Windows authentication, where the operating system credentials are used to identify users. This is a great facility if you are deploying intranet applications and all of your users are in the same Windows domain. However, it’s not applicable for Internet applications.
If we had selected the MVC Internet Application template when we created the SportsStore project, Visual Studio would have created the AccountController class and its LogOn action method for us. The implementation of this method would have used the core ASP.NET membership feature to manage accounts and passwords, which we’ll cover in Chapter 22. Here, the membership system would be overkill for our application, so we will use a simpler approach. We will create the controller ourselves.
To start, we will create a username and password that will grant access to the SportsStore administration features. Listing 9-23 shows the changes to apply to the authentication section of the Web.config file.
23. Listing 9-23. Defining a Username and Password
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880">
<credentials passwordFormat="Clear">
<user name="admin" password="secret" />
</credentials>
</forms>
</authentication>
We have decided to keep things very simple and hard-code a username (admin) and password (secret) in the Web.config file. Most web applications using forms authentication store user credentials in a database, which we show you how to do in Chapter 22. Our focus in this chapter is applying basic security to an MVC application, so hard-coded credentials suit us just fine.
Applying Authorization with Filters
The MVC Framework has a powerful feature called filters. These are .NET attributes that you can apply to an action method or a controller class. They introduce additional logic when a request is processed. Different kinds of filters are available, and you can create your own custom filters, too, as we’ll explain in Chapter 13. The filter that interests us at the moment is the default authorization filter, Authorize. We will apply it to the AdminController class, as shown in Listing 9-24.
24. Listing 9-24. Adding the Authorize Attribute to the Controller Class using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Linq;
namespace SportsStore.WebUI.Controllers { [Authorize]
public class AdminController : Controller { private IProductRepository repository;
public AdminController(IProductRepository repo) { repository = repo;
} ...
When applied without any parameters, the Authorize attribute grants access to the controller action methods if the user is authenticated. This means that if you are authenticated, you are automatically authorized to use the administration features. This is fine for SportsStore, where there is only one set of restricted action methods and only one user. In Chapters 13 and 22, you’ll see how to apply the Authorize filter more selectively to separate the notions of authentication (being identified by the system) and authorized (being allowed to access a given action method).
Note You can apply filters to an individual action method or to a controller. When you apply a filter to a controller, it works as though you had applied it to every action method in the controller class. In Listing 9-24, we applied the Authorize filter to the class, so all of the action methods in the Admin controller are available only to authenticated users.
You can see the effect that the Authorize filter has by running the application and navigating to the /Admin/Index URL. You will see an error similar to the one shown in Figure 9-16.
Figure 9-16. The effect of the Authorize filter
When you try to access the Index action method of the Admin controller, the MVC Framework detects the Authorize filter. Since you have not been authenticated, you are redirected to the URL specified in the Web.config forms authentication section: Account/LogOn. We have not created the Account controller yet, but you can still see that the authentication is working, although it doesn’t prompt us to authenticate ourselves.
Creating the Authentication Provider
Using the forms authentication feature requires us to call two static methods of the System.Web.Security.FormsAuthentication class:
The Authenticate method lets us validate credentials supplied by the user.
The SetAuthCookie method adds a cookie to the response to the browser, so that users don’t need to authenticate every time they make a request.
The problem with calling static methods in action methods is that it makes unit testing the controller difficult.
Mocking frameworks such as Moq can mock only instance members. This problem arises because the FormsAuthentication class predates the unit-testing-friendly design of MVC. The best way to address this is to decouple the controller from the class with the static methods using an interface. An additional benefit is that this fits in with the broader MVC design pattern and makes it easier to switch to a different authentication system later.
We start by defining the authentication provider interface. Create a new folder called Abstract in the
Infrastructure folder of the SportsStore.WebUI project and add a new interface called IAuthProvider. The contents of this interface are shown in Listing 9-25.
25. Listing 9-25. The IAuthProvider Interface namespace SportsStore.WebUI.Infrastructure.Abstract { public interface IAuthProvider {
bool Authenticate(string username, string password);
} }
We can now create an implementation of this interface that acts as a wrapper around the static methods of the FormsAuthentication class. Create another new folder in Infrastructure—this time called Concrete—and create a new class called FormsAuthProvider. The contents of this class are shown in Listing 9-26.
26. Listing 9-26. The FormsAuthProvider Class using System.Web.Security;
using SportsStore.WebUI.Infrastructure.Abstract;
namespace SportsStore.WebUI.Infrastructure.Concrete { public class FormsAuthProvider : IAuthProvider {
public bool Authenticate(string username, string password) {
bool result = FormsAuthentication.Authenticate(username, password);
if (result) {
FormsAuthentication.SetAuthCookie(username, false);
}
return result;
} } }
The implementation of the Authenticate model calls the static methods that we wanted to keep out of the controller. The final step is to register the FormsAuthProvider in the AddBindings method of the
NinjectControllerFactory class, as shown in Listing 9-27 (the addition is shown in bold).
27. Listing 9-27. Adding the Authentication Provider Ninject Binding private void AddBindings() {
// put additional bindings here
ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>();
// create the email settings object
EmailSettings emailSettings = new EmailSettings { WriteAsFile
= bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ?? "false") };
ninjectKernel.Bind<IOrderProcessor>()
.To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings);
}
Creating the Account Controller
The next task is to create the Account controller and the LogOn action method. In fact, we will create two versions of the LogOn method. The first will render a view that contains a login prompt, and the other will handle the POST request when users submit their credentials.
To get started, we will create a view model class that we will pass between the controller and the view. Add a new class to the Models folder of the SportsStore.WebUI project called LogOnViewModel and edit the content so that it matches Listing 9-28.
28. Listing 9-28. The LogOnViewModel Class using System.ComponentModel.DataAnnotations;
namespace SportsStore.WebUI.Models { public class LogOnViewModel { [Required]
public string UserName { get; set; } [Required]
[DataType(DataType.Password)]
public string Password { get; set; } }
}
This class contains properties for the username and password, and uses the data annotations to specify that both are required. In addition, we use the DataType attribute to tell the MVC Framework how we want the editor for the Password property displayed.
Given that there are only two properties, you might be tempted to do without a view model and rely on the ViewBag to pass data to the view. However, it is good practice to define view models so that the data passed from the controller to the view and from the model binder to the action method is typed consistently. This allows us to use template view helpers more easily.
Next, create a new controller called AccountController, as shown in Listing 9-29.
29. Listing 9-29. The AccountController Class using System.Web.Mvc;
using SportsStore.WebUI.Infrastructure.Abstract;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers { public class AccountController : Controller { IAuthProvider authProvider;
public AccountController(IAuthProvider auth) {
authProvider = auth;
}
public ViewResult LogOn() { return View();
} [HttpPost]
public ActionResult LogOn(LogOnViewModel model, string returnUrl) { if (ModelState.IsValid) {
if (authProvider.Authenticate(model.UserName, model.Password)) { return Redirect(returnUrl ?? Url.Action("Index", "Admin"));
} else {
ModelState.AddModelError("", "Incorrect username or password");
return View();
} } else {
return View();
} } } }
Creating the View
Right-click in one of the action methods in the Account controller class and select Add View from the pop-up menu.
Create a strongly typed view called LogOn that uses LogOnViewModel as the view model type, as shown in Figure 9-17. Check the option to use a Razor layout and select _AdminLayout.cshtml.
Figure 9-17. Adding the LogOn view
Click the Add button to create the view and edit the markup so that it matches Listing 9-30.
30. Listing 9-30. The LogOn View
@model SportsStore.WebUI.Models.LogOnViewModel
@{
ViewBag.Title = "Admin: Log In";
Layout = "~/Views/Shared/_AdminLayout.cshtml";
}
<h1>Log In</h1>
<p>Please log in to access the administrative area:</p>
@using(Html.BeginForm()) { @Html.ValidationSummary(true)
@Html.EditorForModel()
<p><input type="submit" value="Log in" /></p>
}
You can see how the view looks in Figure 9-18.
Figure 9-18. The LogOn view
The DataType attribute has led the MVC Framework to render the editor for the Password property as an HTML password-input element, which means that the characters in the password are not visible. The Required attribute that we applied to the properties of the view model are enforced using client-side validation (the required JavaScript libraries are included in the layout). Users can submit the form only after they have provided both a username and password, and the authentication is performed at the server when we call the FormsAuthentication.Authenticate method.
CautionIn general, using client-side data validation is a good idea. It off-loads some of the work from your server and gives users immediate feedback about the data they are providing. However, you should not be tempted to perform authentication at the client, since this would typically involve sending valid credentials to the client so they can be used to check the username and password that the user has entered, or at least trusting the client’s report of whether they have successfully authenticated. Authentication must always be done at the server.
When we receive bad credentials, we add an error to the ModelState and rerender the view. This causes our message to be displayed in the validation summary area, which we have created by calling the
Html.ValidationSummary helper method in the view.
Note Notice that we call the Html.ValidationSummary helper method with a bool parameter value of true in Listing 9-27. Doing so excludes any property validation messages from being displayed. If we had not done this, any property validation errors would be duplicated in the summary area and next to the corresponding input element.