Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC Policy Override in Integration Tests

Tags:

I am in the process of adding integration tests at work for an MVC app. Many of our endpoints have policies applied to them, e.g.

namespace WorkProject
{
  [Route("A/Route")]
  public class WorkController : Controller
  {
    [HttpPost("DoStuff")]
    [Authorize(Policy = "CanDoStuff")]
    public IActionResult DoStuff(){/* */}
  }
}

For our integration tests, I have overridden the WebApplicationFactory like it is suggested in the ASP .NET Core documentation. My goal was to overload the authentication step and to bypass the policy by making a class which allows all parties through the authorization policy.

namespace WorkApp.Tests
{
    public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            base.ConfigureWebHost(builder);
            builder.ConfigureServices(services =>
            {
                services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = "Test Scheme"; // has to match scheme in TestAuthenticationExtensions
                    options.DefaultChallengeScheme = "Test Scheme";
                }).AddTestAuth(o => { });


                services.AddAuthorization(options =>
                {
                    options.AddPolicy("CanDoStuff", policy =>
                        policy.Requirements.Add(new CanDoStuffRequirement()));
                });

             // I've also tried the line below, but neither worked
             // I figured that maybe the services in Startup were added before these
             // and that a replacement was necessary
             // services.AddTransient<IAuthorizationHandler, CanDoStuffActionHandler>();
             services.Replace(ServiceDescriptor.Transient<IAuthorizationHandler, CanDoStuffActionHandler>());
            });
        }
    }

    internal class CanDoStuffActionHandler : AuthorizationHandler<CanDoStuffActionRequirement>
    {
        public CanDoStuffActionHandler()
        {
        }

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CanDoStuffActionRequirement requirement)
        {
            context.Succeed(requirement);

            return Task.CompletedTask;
        }
    }

    internal class CanDoStuffRequirement : IAuthorizationRequirement
    {
    }
}

The first thing that I do to the services is override the authentication as suggested here (without the bit about overriding Startup since that didn't seem to work for me). I am inclined to believe that this authentication override works. When I run my tests, I receive an HTTP 403 from within the xUnit testing framework. If I hit the route that I am testing from PostMan I receive an HTTP 401. I have also made a class that lives in the custom web application factory that allows all requests for the CanDoStuff authorization handler. I thought this would allow the integration tests through the authorization policy, but, as stated above, I receive an HTTP 403. I know that a 403 will be returned if the app doesn't know where certain files are. However, this is a post route strictly for receiving and processing data and this route does not attempt to return any views so this 403 is most likely related to the authorization policy which, for some reason, is not being overridden.

I'm clearly doing something wrong. When I run the test under debug mode and set a breakpoint in the HandleRequirementsAsync function, the application never breaks. Is there a different way that I should be attempting to override the authorization policies?

like image 762
K. Shores Avatar asked May 30 '19 13:05

K. Shores


1 Answers

Here is what I did.

  1. Override the WebApplicationFactory with my own. Note, I still added my application's startup as the template parameter
  2. Create my on startup function which overrides the ConfigureAuthServices function that I added.
  3. Tell the builder in the ConfigureWebHost function to use my custom startup class.
  4. Override the authentication step in the ConfigureWebHost function via builder.ConfigureServices.
  5. Add an assembly reference to the controller whose endpoint I am trying to hit at the end of builder.ConfigureServices in the ConfigureWebHost function.
  6. Write my own IAuthorizationHandler for the policy that allows all requests to succeed.

I hope I have done a decent job at explaining what I did. If not, hopefully the sample code below is easy enough to follow.

YourController.cs

namespace YourApplication
{
  [Route("A/Route")]
  public class WorkController : Controller
  {
    [HttpPost("DoStuff")]
    [Authorize(Policy = "CanDoStuff")]
    public IActionResult DoStuff(){/* */}
  }
}

Test.cs

namespace YourApplication.Tests
{
    public class Tests
        : IClassFixture<CustomWebApplicationFactory<YourApplication.Startup>>
    {
        private readonly CustomWebApplicationFactory<YourApplication.Startup> _factory;

        public Tests(CustomWebApplicationFactory<YourApplication.Startup> factory)
        {
            _factory = factory;
        }

        [Fact]
        public async Task SomeTest()
        {
            var client = _factory.CreateClient();
            var response = await client.PostAsync("/YourEndpoint");
            response.EnsureSuccessStatusCode();

            Assert.Equal(/* whatever your condition is */);
        }
    }
}

CustomWebApplicationFactory.cs

namespace YourApplication.Tests
{
    public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            base.ConfigureWebHost(builder);
            builder.ConfigureServices(services =>
            {
                services.AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = "Test Scheme"; // has to match scheme in TestAuthenticationExtensions
                    options.DefaultChallengeScheme = "Test Scheme";
                }).AddTestAuth(o => { });


                services.AddAuthorization(options =>
                {
                    options.AddPolicy("CanDoStuff", policy =>
                        policy.Requirements.Add(new CanDoStuffRequirement()));
                });

             services.AddMvc().AddApplicationPart(typeof(YourApplication.Controllers.YourController).Assembly);
             services.AddTransient<IAuthorizationHandler, CanDoStuffActionHandler>();
            });
            builder.UseStartup<TestStartup>();
        }
    }

    internal class CanDoStuffActionHandler : AuthorizationHandler<CanDoStuffActionRequirement>
    {
        public CanDoStuffActionHandler()
        {
        }

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CanDoStuffActionRequirement requirement)
        {
            context.Succeed(requirement);

            return Task.CompletedTask;
        }
    }

    internal class CanDoStuffRequirement : IAuthorizationRequirement
    {
    }
}

TestStartup.cs

namespace YourApplication.Tests
{
    public class TestStartup : YourApplication.Startup
    {
        public TestStartup(IConfiguration configuration) : base(configuration)
        {

        }

        protected override void ConfigureAuthServices(IServiceCollection services)
        {
        }
    }
}
like image 186
K. Shores Avatar answered Sep 28 '22 19:09

K. Shores