By using this site, you agree to our updated Privacy Policy and our Terms of Use. Manage your Cookies Settings.
463,098 Members | 695 Online
Bytes IT Community
Submit an Article
Got Smarts?
Share your bits of IT knowledge by writing an article on Bytes.

Testing an ActionFilter for an MVC Controller

P: 221

this is my second article that I'm writing for, and this time I wish to share some knowledge on how to test a custom ActionFilterAttribute for a controller in the ASP.NET MVC 2 Framework.

I've been working with the Framework for a month now, comming from a Ruby on Rails background, and developed a special ActionFilter that performs authentication of users against our database using customized HTTP headers.

The concept of the filter, is to check for the presence of these headers and then extract the required information from these headers to perform the authentication. The code for the filter looks somehow like this:

Expand|Select|Wrap|Line Numbers
  1. using System;
  2. using System.Text;
  3. using System.Web.Mvc;
  4. using TenForce.Execution.Framework;
  5. using TenForce.Execution.Api2.Implementation;
  7. namespace TenForce.Execution.Web.Filters
  8. {
  9.     /// <summary>
  10.     /// This class defines a custom Authentication attribute that can be applied on controllers.
  11.     /// This results in authentication occuring on all actions that are beeing defined in the controller
  12.     /// who implements this filter.
  13.     /// </summary>
  14.     [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
  15.     public class AuthenticationFilter : ActionFilterAttribute
  16.     {
  17.         #region IAuthorizationFilter Members
  19.         /// <summary>
  20.         /// This function get's called by the Mvc framework prior to performing any actions on
  21.         /// the controller. The function will check if a call is authorized by the caller.
  22.         /// The function will extract the username and password from the HTTP headers send by
  23.         /// the caller and will validate these against the database to see if there is a valid
  24.         /// account for the user.
  25.         /// If the user can be found in the database, operations will resume, otherwise the action
  26.         /// is canceled.
  27.         /// </summary>
  28.         /// <param name="filterContext">The context for the filter.</param>
  29.         public override void OnActionExecuting(ActionExecutingContext filterContext)
  30.         {
  31.             // Call the base operations first.
  32.             base.OnActionExecuting(filterContext);
  34.             // Surround the entire authentication process with a try-catch to prevent errors from
  35.             // breaking the code.
  36.             try
  37.             {
  38.                 // Extract the custom authorization header from the HTTP headers.
  39.                 string customAuthHeader = Encoding.UTF8.GetString(Convert.FromBase64String(filterContext.RequestContext.HttpContext.Request.Headers["TenForce-Auth"]));
  41.                 // Split the header in the subcomponents.
  42.                 string[] components = customAuthHeader.Split('|');
  44.                 // Check if both components are present.
  45.                 if (components.Length >= 2)
  46.                 {
  47.                     // This header consists of 2 parts, the username and password, seperate by a vertical pipe.
  48.                     string username = components[0] ?? string.Empty;
  49.                     string password = components[1] ?? string.Empty;
  51.                     // Validate the user against the database.
  52.                     if (Authenticator.Authenticate(username, password))
  53.                     {
  54.                         // The request is valid, so add the custom header to inform the request was
  55.                         // authorized.
  56.                         AllowRequest(filterContext);
  57.                         return;
  58.                     }
  59.                 }
  61.                 // If we reach this point, the authorization request is no longer valid.
  62.                 CancelRequest(filterContext);
  63.             }
  64.             catch (Exception ex)
  65.             {
  66.                 // Log the exception that has occured.
  67.                 Logger.Log(GetType(), ex);
  69.                 // Cancel the request, as we could not properly process it.
  70.                 CancelRequest(filterContext);
  71.             }
  72.         }
  74.         #endregion
  76.         #region Private Methods
  78.         /// <summary>
  79.         /// Cancels the Athorization and adds the custom tenforce header to the response to
  80.         /// inform the caller that his call has been denied.
  81.         /// </summary>
  82.         /// <param name="authContext">The authorizationContxt that needs to be canceled.</param>        
  83.         private static void CancelRequest(ActionExecutingContext authContext)
  84.         {
  85.             authContext.Result = new HttpUnauthorizedResult();
  86.             authContext.HttpContext.Response.Headers.Add(@"Custom Response Header", @"Denied value");
  87.         }
  89.         /// <summary>
  90.         /// Allows the Authorization and adds the custom tenforce header to the response to
  91.         /// inform the claler that his call has been allowed.
  92.         /// </summary>
  93.         /// <param name="authContext">The authorizationContext that needs to be allowed.</param>
  94.         private static void AllowRequest(ActionExecutingContext authContext)
  95.         {
  96.             authContext.Result = null;
  97.             authContext.HttpContext.Response.Headers.Add(@"Custom Response Header", @"Accepted Value");
  98.         }        
  100.         #endregion
  101.     }
  102. }
The code explain it's purpose. However, because we use TDD, we needed a UnitTest that can actually emulate a required for this filter and provide the correct attributes, without the filter knowing that it's working with simulated data.
The anwser to this problem : Mock Frameworks.

For the following code, we will rely on the following Frameworks to properly support the code:
- MoQ 4
- Gillio
- Our own Framework (to handle database traffic)

Step:1 Constructing the Mock Objects
The first step in properly testing the functionality of the ActionFilter that was shown above, is to construct the Mock objects that will represent the HttpContext object. In the MVC Framework, this object contains all the information related to a specific HTTP Request or Response. The Tricky part however, is that various properties are read only and cannot be set directly without constructing huge wrappers around it.

So in order to construct the object, the response and the request we use the following calls from the Mock Framework:
Expand|Select|Wrap|Line Numbers
  1. HttpRequest = new Mock<HttpRequestBase>();
  2.             HttpResponse = new Mock<HttpResponseBase>();
  3.             HttpContext = new Mock<HttpContextBase>();
  4.             ActionContext = new Mock<ActionExecutingContext>();
  5.             Filter = new Web.Filters.AuthenticationFilter();
Note that these values are stored in properties of the Test class.

On their own, these objects don't do much. We need to tell these Mock objects how to respond to requests from the Filter, otherwise we would be bombarded with null reference exceptions.
When you look at the code for the Filter, it becomes evidence that we need to have access to the following parts:
- The Response and Request property of the HttpContext
- The Headers property of the Response object
- The Headers property of the Request object
- The URL that was used to request the authentication

Because each object is a sub-object of the global ActionContext object, we need to build up our tree of requests step by step. The following code demonstrates how we link the objects together:
Expand|Select|Wrap|Line Numbers
  1. ActionContext.SetupGet(c => c.HttpContext).Returns(HttpContext.Object);
  2.             HttpContext.SetupGet(r => r.Request).Returns(HttpRequest.Object);
  3.             HttpContext.SetupGet(r => r.Response).Returns(HttpResponse.Object);
  4.             HttpResponse.SetupGet(x => x.Headers).Returns(new System.Net.WebHeaderCollection());
  5.             HttpRequest.SetupGet(r => r.RawUrl).Returns(@"");
Now we have configured our Mock objects to know each other and return something that can be used when a call is made to the Headers, Request or Response property.

Step 2 : performing the unit test
Now we need to actually call the test. Because the filter is a class, we can create an instance from it and run the code with a UnitTest Framework such as NUnit:

Expand|Select|Wrap|Line Numbers
  1. [Test]
  2.         public void SuccessfullAuthentication()
  3.         {
  4.             // Configure the Request and Response headers before making the call
  5.             // to the ActionFilter. Ensure the authentication header is present.
  6.             HttpRequest.SetupGet(r => r.Headers).Returns(new System.Net.WebHeaderCollection
  7.                                                              {{@"YourHeader", "Header value"}});
  9.             // Call the action on the filter and check the response.
  10.             Filter.OnActionExecuting(ActionContext.Object);
  12.             // Check the ActionResult to null and that the response header contains the correct value.
  13.             Assert.IsTrue(ActionContext.Object.Result == null);
  14.             Assert.IsTrue(ActionContext.Object.HttpContext.Response.Headers["Response Header"].Equals(@"Response Header Value"));
  15.         }
What this code does, is call the function that the MVC Framework would call when a request is received that requires authentication.

With the first line of code, we mimic the call of the HTTP Request and insert the custom header that is required by the ActionFilter with the correct value.

The second line of code calls the actuall filter code and performs the entire authentication process that can be seen in the first code snippet.

The hardest part is getting the Mock Framework properly configured in order to mimic the MVC Calls. BEcause I struggled with this myself, and the solution is soo straightforward, I decided to share this insight with everyone.
Dec 27 '10 #1
Share this Article
Share on Google+