【问题标题】:Unit testing IAuthenticationFilter in WebApi 2WebApi 2 中的单元测试 IAuthenticationFilter
【发布时间】:2014-09-01 05:59:15
【问题描述】:

我正在尝试对我为 WebApi 2 项目编写的基本身份验证过滤器进行单元测试,但我无法模拟 OnAuthentication 调用中所需的 HttpAuthenticationContext 对象。

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

    var authHeader = context.Request.Headers.Authorization;

    ... the rest of my code here
}

我试图为模拟设置的实现中的那一行是设置 authHeader 变量的那一行。

但是,我不能模拟 Headers 对象,因为它是密封的。而且我无法模拟请求并设置模拟标头,因为它是非虚拟属性。依此类推,一直到上下文。

是否有人成功地对新的 IAuthenticationFilter 实现进行了单元测试?

我正在使用 Moq,但如果您有示例代码,我相信我可以在任何模拟库中跟进。

感谢您的帮助。

【问题讨论】:

  • 如果这没有成功,只是一个建议。您可以在单元测试中创建一个 owin 服务器,并使用伪造的用户身份验证令牌向控制器操作发出请求,并查看用户是否通过了授权过滤器。如果您唯一要测试的就是这个过滤器,那就有点麻烦了。

标签: c# unit-testing asp.net-web-api moq


【解决方案1】:

可以实现您想要的,但是链上下文中的任何对象都没有。Request.Headers.Authorization 公开虚拟属性 Mock 或任何其他框架都不会为您提供太多帮助。以下是使用模拟值获取 HttpAuthenticationContext 的代码:

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;

您只需要以普通方式创建某些对象并使用构造函数或属性将它们传递给其他对象。我创建 HttpControllerContext 和 HttpActionContext 实例的原因是因为 HttpAuthenticationContext.Request 属性只有获取部分 - 它的值可以通过 HttpControllerContext 设置。使用上面的方法,您可能会测试您的过滤器,但是您无法在测试中验证上面对象的某些属性是否仅仅因为它们不可覆盖而被触摸 - 否则就不可能跟踪这一点。

【讨论】:

    【解决方案2】:

    我能够使用来自 @mr100 的答案开始解决我的问题,即对几个 IAuthorizationFilter 实现进行单元测试。为了有效地对 web api 授权进行单元测试,您不能真正使用 AuthorizationFilterAttribute 并且您必须应用一个全局过滤器来检查控制器/操作上是否存在被动属性。长话短说,我扩展了来自@mr100 的答案,包括控制器/动作描述符的模拟,让您可以在有/没有属性的情况下进行测试。举例来说,我将包含我需要进行单元测试的两个过滤器中更简单的一个,它强制指定控制器/操作的 HTTPS 连接(或者如果需要,可以全局连接):

    这是在您想要强制 HTTPS 连接时应用的属性,请注意它不会做任何事情(它是被动的):

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

    这是在每个请求上检查属性是否存在以及连接是否通过 HTTPS 的过滤器:

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

    最后一个测试证明它在需要但不使用 https 时返回 403 禁止状态(这里使用了很多 @mr100 的答案):

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-06
      • 1970-01-01
      • 2014-02-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多