RequestContext.Principal 的 Autofac DI 在单元测试中使用 WebAPI2

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

我在一个从头开始构建的 WebAPI 项目中使用 Autofac 和 OWIN(与 VS2015 中可用的完整 WebAPI 模板相对)。不可否认,我是这样做的新手。

在单元测试项目中,我在单元测试开始时设置了一个OWIN启动class:

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

启动class如下:

[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);
        }
    }
}

旁注:理想情况下,我想将 Principle 设置为测试的一部分,以便能够将不同的用户传递给控制器​​,但如果我绝对必须保留 CurrentPrincipal/User 在启动 class 中,我可以解决它。

启动 class 工作正常 w.r.t 使用 DI 访问我的控制器,但是从未设置 RequestContext.Principal 中的主体。它始终为空。我打算使用请求上下文的方式如下:

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

我还尝试将模拟主体 class 注入到我的 Controller 的构造函数中作为解决方法 - 我使用了与通用方法中显示的相同方法来使用 DI 设置我的服务。然而,我再次在我的构造函数中得到了空值。

在这一点上,我已经为这个问题坐了大约一天,拔掉了我的头发。任何帮助,将不胜感激。提前致谢。

我会避免使用 DI 执行此操作。 您需要在请求上下文中设置主体,而不是将主体注入构造函数。

如果是我,我会这样做:

首先,我不会模拟不需要模拟的东西。也就是说,您的 IIdentity 实现实际上可能是真实对象。

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

接下来,您需要 运行 在您通过测试应用程序处理的每个 "request" 上进行设置。 我猜这是更多 "integration test" 比 "unit test" 因为你使用的是整个启动 class 和所有东西,所以你不能只设置一次主体就完成了。它必须在每个请求上完成,就像真正的身份验证操作一样。

最简单的方法是使用 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);
  }
}

最后,将该处理程序添加到 Startup class.

中的 HttpConfiguration 管道
config.MessageHandlers.Add(new TestAuthHandler());

应该可以了。请求现在应该通过该身份验证处理程序并分配主体。