Тестирование модуля IAuthenticationFilter в WebApi 2

Я пытаюсь использовать unit test базовый фильтр проверки подлинности, который я написал для проекта WebApi 2, но у меня возникли проблемы с издевательством над объектом HttpAuthenticationContext, необходимым для вызова OnAuthentication.

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, но я уверен, что смогу следовать в любой насмешливой библиотеке, если у вас есть пример кода.

Спасибо за любую помощь.

Ответ 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 имеет только часть get - его значение может быть установлено через HttpControllerContext. Используя описанный выше метод, вы можете протестировать свой фильтр, однако вы не можете проверить в тесте, были ли затронуты определенные свойства объектов выше, потому что они не могут быть переопределены - без этого нет возможности отследить это.

Ответ 2

Я смог использовать ответ от @mr100, чтобы начать работу над решением моей проблемы, которая была модульной проверкой нескольких реализаций IAuthorizationFilter. Чтобы эффективно разрешить авторизацию unit test web api, вы не можете использовать AuthorizationFilterAttribute, и вам нужно применить глобальный фильтр, который проверяет наличие пассивных атрибутов на контроллерах/действиях. Короче говоря, я расширил ответ от @mr100, чтобы включить mocks для дескрипторов контроллера/действия, которые позволяют вам тестировать с/без наличия ваших атрибутов. В качестве примера я включу более простой из двух фильтров, необходимых мне для unit test, который заставляет 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();            
    }
}

И, наконец, тест, подтверждающий его, возвращает статус 403, если https требуется, но не используется (используя много ответов @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);
}