Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing IAuthenticationFilter in WebApi 2

I'm trying to unit test a basic authentication filter I've written for a WebApi 2 project, but i'm having trouble mocking the HttpAuthenticationContext object required in the OnAuthentication call.

public override void OnAuthentication(HttpAuthenticationContext context)
{
    base.OnAuthentication(context);

    var authHeader = context.Request.Headers.Authorization;

    ... the rest of my code here
}

The line in the implementation that I'm trying to set up for mocking is the one that sets the authHeader variable.

However, I can't mock the Headers object because its sealed. And I can't mock the request and set a mocked headers because its a non-virtual property. And so on up the chain all the way to the context.

Has anyone successfully unit tested a new IAuthenticationFilter implementation?

I'm using Moq but I'm sure I could follow along in any mocking library if you have sample code.

Thanks for any help.

like image 632
Gobie74 Avatar asked Jul 10 '14 18:07

Gobie74


People also ask

Is it possible to unit test Web API?

This doesn't necessarily need to be a bad thing and there are ways to improve our code and make it more maintainable. Unit testing plays a very important role in making the software more maintainable. Our intention in this post is to make an intro to unit testing of the ASP.NET Core Web API application.


2 Answers

It is possible to achieve what you wanted however as none of the objects in the chain context.Request.Headers.Authorization exposes virtual properties Mock or any other framework won't provide much help for you. Here is the code for obtaining HttpAuthenticationContext with mocked values:

HttpRequestMessage request = new HttpRequestMessage();
HttpControllerContext controllerContext = new HttpControllerContext();
controllerContext.Request = request;
HttpActionContext context = new HttpActionContext();
context.ControllerContext = controllerContext;
HttpAuthenticationContext m = new HttpAuthenticationContext(context, null);
HttpRequestHeaders headers = request.Headers;
AuthenticationHeaderValue authorization = new AuthenticationHeaderValue("scheme");
headers.Authorization = authorization;

You just simply need to create in ordinary fashion certain objects and pass them to other with constructors or properties. The reason why I created HttpControllerContext and HttpActionContext instances is because HttpAuthenticationContext.Request property has only get part - its value may be set through HttpControllerContext. Using the method above you might test your filter, however you cannot verify in the test if the certain properties of objects above where touched simply because they are not overridable - without that there is no possibility to track this.

like image 97
mr100 Avatar answered Oct 11 '22 10:10

mr100


I was able to use the answer from @mr100 to get me started in solving my problem which was unit testing a couple of IAuthorizationFilter implementations. In order to effectively unit test web api authorization you can't really use AuthorizationFilterAttribute and you have to apply a global filter that check for the presence of passive attributes on controllers/actions. Long story short, I expanded on the answer from @mr100 to include mocks for the controller/action descriptors that let you test with/without the presence of your attributes. By way of example I will include the simpler of the two filters I needed to unit test which forces HTTPS connections for specified controllers/actions (or globally if you want):

This is the attribute that is applied where ever you want to force an HTTPS connection, note that it doesn't do anything (it's passive):

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class HttpsRequiredAttribute : Attribute
{       
    public HttpsRequiredAttribute () { }
}

This is the filter that on every request checks to see if the attribute is present and if the connection is over HTTPS or not:

public class HttpsFilter : IAuthorizationFilter
{
    public bool AllowMultiple => false;

    public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
    {
        List<HttpsRequiredAttribute> action = actionContext.ActionDescriptor.GetCustomAttributes<HttpsRequiredAttribute>().ToList();
        List<HttpsRequiredAttribute> controller = actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<HttpsRequiredAttribute>().ToList();

        // if neither the controller or action have the HttpsRequiredAttribute then don't bother checking if connection is HTTPS
        if (!action.Any() && !controller.Any())
            return continuation();

        // if HTTPS is required but the connection is not HTTPS return a 403 forbidden
        if (!string.Equals(actionContext.Request.RequestUri.Scheme, "https", StringComparison.OrdinalIgnoreCase))
        {
            return Task.Factory.StartNew(() => new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
            {
                ReasonPhrase = "Https Required",
                Content = new StringContent("Https Required")
            });
        }

        return continuation();            
    }
}

And finally a test to prove it returns a status of 403 forbidden when https is required but not used (using a lot of @mr100's answer here):

[TestMethod]
public void HttpsFilter_Forbidden403_WithHttpWhenHttpsIsRequiredByAction()
{
    HttpRequestMessage requestMessage = new HttpRequestMessage();
    requestMessage.SetRequestContext(new HttpRequestContext());
    requestMessage.RequestUri = new Uri("http://www.some-uri.com"); // note the http here (not https)

    HttpControllerContext controllerContext = new HttpControllerContext();
    controllerContext.Request = requestMessage;

    Mock<HttpControllerDescriptor> controllerDescriptor = new Mock<HttpControllerDescriptor>();
    controllerDescriptor.Setup(m => m.GetCustomAttributes<HttpsRequiredAttribute>()).Returns(new Collection<HttpsRequiredAttribute>()); // empty collection for controller

    Mock<HttpActionDescriptor> actionDescriptor = new Mock<HttpActionDescriptor>();
    actionDescriptor.Setup(m => m.GetCustomAttributes<HttpsRequiredAttribute>()).Returns(new Collection<HttpsRequiredAttribute>() { new HttpsRequiredAttribute() }); // collection has one attribute for action
    actionDescriptor.Object.ControllerDescriptor = controllerDescriptor.Object;

    HttpActionContext actionContext = new HttpActionContext();
    actionContext.ControllerContext = controllerContext;
    actionContext.ActionDescriptor = actionDescriptor.Object;

    HttpAuthenticationContext authContext = new HttpAuthenticationContext(actionContext, null);

    Func<Task<HttpResponseMessage>> continuation = () => Task.Factory.StartNew(() => new HttpResponseMessage() { StatusCode = HttpStatusCode.OK });

    HttpsFilter filter = new HttpsFilter();
    HttpResponseMessage response = filter.ExecuteAuthorizationFilterAsync(actionContext, new CancellationTokenSource().Token, continuation).Result;

    Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode);
}
like image 1
Ben Avatar answered Oct 11 '22 11:10

Ben