Ebbs
Ebbs

Reputation: 1050

Autofac DI for RequestContext.Principal using WebAPI2 in a unit test

I am using Autofac and OWIN in a WebAPI project which was build from scratch (as apposed to the complete WebAPI template available in VS2015). Admittedly I am new to doing it this way.

In the unit test project, I set up an OWIN Startup class at the start of unit testing:

WebApp.Start<Startup>("http://localhost:9000/")

The Startup class is as follows:

[assembly: OwinStartup(typeof(API.Specs.Startup))]

namespace API.Specs
{
    public class Startup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            var config = new HttpConfiguration();
            //config.Filters.Add(new AccessControlAttribute());
            config.Services.Replace(typeof(IAssembliesResolver), new CustomAssembliesResolver());

            config.Formatters.JsonFormatter.SerializerSettings = Serializer.Settings;
            config.MapHttpAttributeRoutes();

            // Autofac configuration
            var builder = new ContainerBuilder();

            // Unit of Work
            var unitOfWork = new Mock<IUnitOfWork>();
            builder.RegisterInstance(unitOfWork.Object).As<IUnitOfWork>();

            //  Principal
            var principal = new Mock<IPrincipal>();
            principal.Setup(p => p.IsInRole("admin")).Returns(true);
            principal.SetupGet(p => p.Identity.Name).Returns('test.user');
            principal.SetupGet(p => p.Identity.IsAuthenticated).Returns(true);

            Thread.CurrentPrincipal = principal.Object;
            if (HttpContext.Current != null)
            {
                HttpContext.Current.User = new GenericPrincipal(principal.Object.Identity, null);
            }

            builder.Register(c => principal).As<IPrincipal>();

            .
            .
            .
            // Set up dependencies for Controllers, Services & Repositories
            .
            .
            .

            var container = builder.Build();
            config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
            config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;

            appBuilder.UseWebApi(config);
        }

        private static void RegisterAssemblies<TModel, TController, TService, TRepoClass, TRepoInterface>(ref ContainerBuilder builder, ref Mock<IUnitOfWork> unitOfWork) 
            where TModel : class 
            where TRepoClass : class
            where TService : class
        {
            RegisterController<TController>(ref builder);
            var repositoryInstance = RegisterRepository<TRepoClass, TRepoInterface>(ref builder);
            RegisterService<TService>(ref builder, ref unitOfWork, repositoryInstance);
        }

        private static void RegisterController<TController>(ref ContainerBuilder builder) 
        {
            builder.RegisterApiControllers(typeof(TController).Assembly);
        }

        private static object RegisterRepository<TRepoClass, TRepoInterface>(ref ContainerBuilder builder) 
            where TRepoClass : class
        {
            var constructorArguments = new object[] { DataContexts.Instantiate };
            var repositoryInstance = Activator.CreateInstance(typeof(TRepoClass), constructorArguments);
            builder.RegisterInstance(repositoryInstance).As<TRepoInterface>();

            return repositoryInstance;
        }

        private static void RegisterService<TService>(ref ContainerBuilder builder, ref Mock<IUnitOfWork> unitOfWork, object repositoryInstance)
            where TService : class
        {
            var constructorArguments = new[] { repositoryInstance, unitOfWork.Object};
            var serviceInstance = Activator.CreateInstance(typeof(TService), constructorArguments);

            builder.RegisterAssemblyTypes(typeof(TService).Assembly)
                .Where(t => t.Name.EndsWith("Service"))
                .AsImplementedInterfaces().InstancePerRequest();

            builder.RegisterInstance(serviceInstance);
        }
    }
}

Side note: Ideally I would like to set the Principle as part of the test, so as to be able to pass different users to the controller, but if I absolutely have to keep the setting of the CurrentPrincipal/User in the startup class, I can work around it.

The startup class works fine w.r.t accessing my controllers using DI, however the Principal in RequestContext.Principal is never set. It is always null. The way I intent to use the Request context is as follows:

[HttpGet]
[Route("path/{testId}")]
[ResponseType(typeof(Test))]
public IHttpActionResult Get(string testId)
{
    return Ok(_service.GetById(testId, RequestContext.Principal.Identity.Name));
}

I have also tried injecting the mocked principal class into the constructor of my Controller as a workaround - I used the same method as displayed in the generic method for setting up my services using DI. Again however, I only got null in my constructor.

At this point I have been sitting for about a day with this problem and pulling out my hair. Any help would be appreciated. Thanks in advance.

Upvotes: 1

Views: 818

Answers (1)

Travis Illig
Travis Illig

Reputation: 23894

I'd avoid doing this with DI. You need something to set the principal in the request context, not inject the principal into a constructor.

Here's what I'd do if it was me:

First, I won't mock things that don't need mocking. Which is to say, your IIdentity implementation could actually be real objects.

private static IPrincipal CreatePrincipal()
{
  var identity = new GenericIdentity("test.user", "test");
  var roles = new string[] { "admin" };
  return new GenericPrincipal(identity);
}

Next, you need to run the setup on each "request" you process through your test app. I'm guessing this is more "integration test" than "unit test" since you're using a whole startup class and everything, so you can't just set the principal one time and be done. It has to be done on every request, just like a real authentication action would do.

The easiest way to do that is with a simple delegating handler.

public class TestAuthHandler : DelegatingHandler
{
  protected override async Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken)
  {
    // Set the principal. Whether you set the thread principal
    // is optional but you should really use the request context
    // principal exclusively when checking permissions.
    request.GetRequestContext().Principal = CreatePrincipal();

    // Let the request proceed through the rest of the pipeline.
    return await base.SendAsync(request, cancellationToken);
  }
}

Finally, add that handler to the HttpConfiguration pipeline in your Startup class.

config.MessageHandlers.Add(new TestAuthHandler());

That should do it. Requests should now go through that auth handler and get the principal assigned.

Upvotes: 2

Related Questions