简体   繁体   English

WebApi 2中的单元测试IAuthenticationFilter

[英]Unit testing IAuthenticationFilter in WebApi 2

I'm trying to unit test a basic authentication filter I've written for a WebApi 2 project, but i'm having trouble mocking the HttpAuthenticationContext object required in the OnAuthentication call. 我正在尝试对为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
}

The line in the implementation that I'm trying to set up for mocking is the one that sets the authHeader variable. 我要为模拟设置的实现中的那一行是设置authHeader变量的那一行。

However, I can't mock the Headers object because its sealed. 但是,我无法嘲笑Headers对象,因为它是密封的。 And I can't mock the request and set a mocked headers because its a non-virtual property. 而且我无法模拟请求并设置模拟标头,因为它是非虚拟属性。 And so on up the chain all the way to the context. 依此类推,一直到上下文。

Has anyone successfully unit tested a new IAuthenticationFilter implementation? 有没有人成功地对新的IAuthenticationFilter实现进行了单元测试?

I'm using Moq but I'm sure I could follow along in any mocking library if you have sample code. 我正在使用Moq,但是如果您有示例代码,我敢肯定我可以在任何模拟库中继续学习。

Thanks for any help. 谢谢你的帮助。

It is possible to achieve what you wanted however as none of the objects in the chain context.Request.Headers.Authorization exposes virtual properties Mock or any other framework won't provide much help for you. 但是可以实现所需的目标,因为链上下文中没有任何对象。Request.Headers.Authorization不会公开虚拟属性Mock或任何其他框架都不会为您提供太多帮助。 Here is the code for obtaining HttpAuthenticationContext with mocked values: 这是用于获取具有模拟值的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;

You just simply need to create in ordinary fashion certain objects and pass them to other with constructors or properties. 您只需要以常规方式创建某些对象,然后将它们通过构造函数或属性传递给其他对象。 The reason why I created HttpControllerContext and HttpActionContext instances is because HttpAuthenticationContext.Request property has only get part - its value may be set through HttpControllerContext. 我创建HttpControllerContext和HttpActionContext实例的原因是因为HttpAuthenticationContext.Request属性只有一部分-其值可以通过HttpControllerContext设置。 Using the method above you might test your filter, however you cannot verify in the test if the certain properties of objects above where touched simply because they are not overridable - without that there is no possibility to track this. 使用上面的方法,您可能会测试过滤器,但是您无法在测试中验证是否仅因为它们不可覆盖而触摸了对象上方的某些属性-如果没有,则无法跟踪它。

I was able to use the answer from @mr100 to get me started in solving my problem which was unit testing a couple of IAuthorizationFilter implementations. 我能够使用@ mr100的答案来开始解决我的问题,该问题是对几个IAuthorizationFilter实现进行单元测试。 In order to effectively unit test web api authorization you can't really use AuthorizationFilterAttribute and you have to apply a global filter that check for the presence of passive attributes on controllers/actions. 为了有效地对Web api授权进行单元测试,您不能真正使用AuthorizationFilterAttribute,而必须应用全局过滤器来检查控制器/动作上是否存在被动属性。 Long story short, I expanded on the answer from @mr100 to include mocks for the controller/action descriptors that let you test with/without the presence of your attributes. 长话短说,我扩展了@ mr100的答案,包括了控制器/动作描述符的模拟,可让您测试是否存在属性。 By way of example I will include the simpler of the two filters I needed to unit test which forces HTTPS connections for specified controllers/actions (or globally if you want): 举例来说,我将包括单元测试所需的两个过滤器中的更简单的一个,它可以对指定的控制器/动作(或如果需要的话,可以全局)强制HTTPS连接:

This is the attribute that is applied where ever you want to force an HTTPS connection, note that it doesn't do anything (it's passive): 这是应用于您要强制HTTPS连接的属性,请注意,它不执行任何操作(它是被动的):

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

This is the filter that on every request checks to see if the attribute is present and if the connection is over HTTPS or not: 这是在每个请求上检查以查看该属性是否存在以及连接是否通过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();            
    }
}

And finally a test to prove it returns a status of 403 forbidden when https is required but not used (using a lot of @mr100's answer here): 最后是一个测试,以证明当需要https而不使用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);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM