在WebApi 2中对IAuthenticationFilter进行单元测试

时间:2014-07-10 18:20:36

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

我试图对我为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,但如果你有示例代码,我确信我可以在任何模拟库中跟进。

感谢您的帮助。

2 个答案:

答案 0 :(得分:18)

可以实现你想要的,但链中的所有对象都没有.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设置。使用上面的方法,您可能会测试您的过滤器,但是您无法在测试中验证上面的对象的某些属性是否仅仅因为它们不可覆盖而被触及 - 如果没有这种属性,则无法跟踪它。

答案 1 :(得分:0)

我能够使用@ 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&#39;)

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