Unit testing an AuthorizeAttribute on an ASP.NET Core MVC API controller
Asked Answered
E

3

28

I have a ASP.NET Core MVC API with controllers that need to be unit tested.

Controller:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace TransitApi.Api.Controllers
{
    [Route("api/foo")]
    public class FooController : Controller
    {
        private IFooRepository FooRepository { get; }

        public FooController(IFooRepository fooRepository)
        {
            FooRepository = fooRepository;
        }

        [HttpGet]
        [Authorize("scopes:getfoos")]
        public async Task<IActionResult> GetAsync()
        {
            var foos = await FooRepository.GetAsync();
            return Json(foos);
        }
    }
}

It is essential that I am able to unit test the effectiveness of the AuthorizeAttribute. We have had issues in our code base with missing attributes and incorrect scopes. This answer is exactly what I am looking for, but not having a ActionInvoker method in Microsoft.AspNetCore.Mvc.Controller means I am not able to do it this way.

Unit Test:

[Fact]
public void GetAsync_InvalidScope_ReturnsUnauthorizedResult()
{
    // Arrange
    var fooRepository = new StubFooRepository();
    var controller = new FooController(fooRepository)
    {
        ControllerContext = new ControllerContext
        {
            HttpContext = new FakeHttpContext()
            // User unfortunately not available in HttpContext
            //,User = new User() { Scopes = "none" }
        }
    };

    // Act
    var result = controller.GetAsync().Result;

    // Assert
    Assert.IsType<UnauthorizedResult>(result);
}

How can I unit test that users without the correct scopes are denied access to my controller method?

Currently I have settled for testing merely the presence of an AuthorizeAttribute as follows, but this is really not good enough:

    [Fact]
    public void GetAsync_Analysis_HasAuthorizeAttribute()
    {
        // Arrange
        var fooRepository = new StubFooRepository();
        var controller = new FooController(fooRepository)
        {
            ControllerContext = new ControllerContext
            {
                HttpContext = new FakeHttpContext()
            }
        };

        // Act
        var type = controller.GetType();
        var methodInfo = type.GetMethod("GetAsync", new Type[] { });
        var attributes = methodInfo.GetCustomAttributes(typeof(AuthorizeAttribute), true);

        // Assert
        Assert.True(attributes.Any());
    }
Edgerton answered 1/2, 2018 at 12:7 Comment(7)
This would need integration testing with an in-memory test server.Readjust
Why is testing for the presence of AuthorizeAttribute not good enough? AuthorizeAttribute is both an attribute and an IAuthorizationFilter. The attribute part doesn't do anything, it is just meta-data. MVC's unit tests guarantee that if it is present, it will be registered as an authorization filter for the current request and the logic run. If you were using a subclass of AuthorizeAttribute, then it would make sense to test its logic, but since you are not the only thing you need to test is the presence of the attribute and configuration of its properties (Users and Groups).Chuckchuckfull
To second NightOwl888's comment, I have created tests that scans all my controller actions to make sure that they ALL have some explicit authorization defined, be it AllowAnonymous or Authorized. Mind you it was for MVC5, I still have to port it to core.Readjust
There is something appealing about being able to unit test receiving specific HTTP response. For example, to ensure noone accidentally changes the authorization scope. But I concede that may have to be done as an integration test. So there's an answer I can accept, could you recommend an elegant way to scan all the controller actions for authorization as @Readjust suggests? I'd rather not have to create individual tests for every action...Edgerton
@Ivan - If you need all of your methods but a few authorized, then you can globally register AuthorizeAttribute at startup, and then use AllowAnonymous to override the behavior. That way, they are locked down by default and you won't need to worry about a later change missing one. Alternatively, you could create your own custom IAuthorizationFilter registered globally that manages the security for the entire application (and possibly even your own attributes to do certain things), which could then be tested as a separate piece than your controllers and actions.Chuckchuckfull
Users have scopes that allow them access to methods individually. For example, a user with a scope:bar may get bars but not foos and vice versa, and a user with a scope:all can access both. That's partly why testing these attributes is so important.Edgerton
What did you end up doing? You seem to have bitten on the "you have to use integration tests" strategy given here, but also left a comment on this non-integration (less integrated?) method that works in MVC (not Core) wondering how to do the same in Core. Did you decide not to port that answer's logic?Noun
R
16

This would need integration testing with an in-memory test server because the attribute is evaluated by the framework as it processes the request pipeline.

Integration testing in ASP.NET Core

Integration testing ensures that an application's components function correctly when assembled together. ASP.NET Core supports integration testing using unit test frameworks and a built-in test web host that can be used to handle requests without network overhead.

[Fact]
public async Task GetAsync_InvalidScope_ReturnsUnauthorizedResult() {
    // Arrange
    var server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
    var client = server.CreateClient();
    var url = "api/foo";
    var expected = HttpStatusCode.Unauthorized;

    // Act
    var response = await client.GetAsync(url);

    // Assert
    Assert.AreEqual(expected, response.StatusCode);
}

You can also create a start up specifically for the test that will replace any dependencies for DI with stubs/mocks if you do not want the test hitting actual production implementations.

Readjust answered 1/2, 2018 at 12:36 Comment(1)
Thanks - we have set up our integration tests as Postman collections. We will use these to test the scopes.Edgerton
U
9

What you could do, is to configure your testserver to add an anonymous filter middleware:

private HttpClient CreatControllerClient()
{
        return _factory.WithWebHostBuilder(builder
            => builder.ConfigureTestServices(services =>
            {
                // allow anonymous access to bypass authorization
                services.AddMvc(opt => opt.Filters.Add(new AllowAnonymousFilter()));
            })).CreateClient();
}
Unset answered 5/4, 2019 at 18:37 Comment(0)
P
0

First remeove IAuthorizationHandler

var authorizationDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IAuthorizationHandler));
                if (authorizationDescriptor != null)
                    services.Remove(authorizationDescriptor);

Then add

services.AddScoped<IAuthorizationHandler, TestAllowAnonymous>();


public class TestAllowAnonymous : IAuthorizationHandler
        {
            public Task HandleAsync(AuthorizationHandlerContext context)
            {
                foreach (IAuthorizationRequirement requirement in context.PendingRequirements.ToList())
                    context.Succeed(requirement); //Simply pass all requirementsreturn Task.CompletedTask;
                return Task.CompletedTask;
            }


        }
Phoneme answered 10/2, 2022 at 14:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.