AntiForgeryToken для интеграционного тестирования в ASP.NET Core

Внимание

Данный материал является частью цикла статей «Тестирование в ASP.NET Core». Не забудьте посмотреть другие статьи по этой теме :-)

  1. Модульное тестирование с помощью xUnit в ASP.NET Core MVC
  2. Тестирование контроллеров MVC в ASP.NET Core
  3. Интеграционное тестирование в ASP.NET Core MVC
  4. AntiForgeryToken для интеграционного тестирования в ASP.NET Core
  5. Тестирование UI с помощью Selenium в ASP.NET Core MVC

В предыдущей статье, мы узнали, как писать интеграционные тесты для различных действий (Index и Create), но пока тестировали действие Create(POST), мы столкнулись с проблемой проверки AntiForgeryToken. Мы пропустили эту проблему, закомментировав этот атрибут проверки, и наш тест прошел, но это было временное решение.

В этой статье мы собираемся решить эту проблему. Мы узнаем, как извлечь AntiForgeryToken из ответа HTML и как использовать его в наших тестах. После исправления проблемы мы сможем протестировать наши действия, защищенные атрибутами проверки того, что запрос не является поддельным.

Внедрение AntiForgeryToken в IserviceCollection

Для начала давайте создадим класс AntiForgeryTokenExtractor в проекте EmployeesApp.IntegrationTests с двумя свойствами:

public static class AntiForgeryTokenExtractor
{
    public static string AntiForgeryFieldName { get; } = "AntiForgeryTokenField";
    public static string AntiForgeryCookieName { get; } = "AntiForgeryTokenCookie";
}

В этом классе мы собираемся обернуть всю логику, необходимую для извлечения поля защиты от подделки и cookie.

Сейчас мы просто определяем поле и имена файлов cookie. Чуть позже мы добавим дополнительные методы. Но пока перейдем к классу TestingWebAppFactory и добавим данные нашего токена в IServiceCollection.

Итак, давайте напишем наш код прямо под частью services.AddDbContext<EmployeeContext>:

services.AddAntiforgery(t =>
{
    t.Cookie.Name = AntiForgeryTokenExtractor.AntiForgeryCookieName;
    t.FormFieldName = AntiForgeryTokenExtractor.AntiForgeryFieldName;
});

С помощью этого кода мы добавляем службу защиты от подделки в указанный IServiceCollection с файлом cookie и именами полей. Как только мы это сделаем, мы сможем извлечь эти свойства из HTML-ответа, используя те же имена, что и объявленные в классе AntiForgeryTokenExtractor.

Извлечение поля и файла cookie из ответа HTML

С учетом сказанного, давайте вернемся к классу AntiForgeryTokenExtractor и сначала добавим необходимый код для извлечения файла cookie:

private static string ExtractAntiForgeryCookieValueFrom(HttpResponseMessage response)
{
    string antiForgeryCookie = response.Headers.GetValues("Set-Cookie")
        .FirstOrDefault(x => x.Contains(AntiForgeryCookieName));
    if (antiForgeryCookie is null)
    {
        throw new ArgumentException($"Cookie '{AntiForgeryCookieName}' not found in HTTP response", nameof(response));
    }

    string antiForgeryCookieValue = SetCookieHeaderValue.Parse(antiForgeryCookie).Value.ToString();

    return antiForgeryCookieValue;
}

В нашем коде мы извлекаем значение свойства Set-Cookie заголовка нашего ответа, который содержит имя определенного файла cookie. После этого, если этот файл cookie не существует, мы генерируем исключение. В противном случае мы просто разбираем его значение и возвращаем его.

Теперь мы можем добавить еще один метод для извлечения поля:

private static string ExtractAntiForgeryToken(string htmlBody)
{
    var requestVerificationTokenMatch = Regex.Match(htmlBody, $@"\<input name=""{AntiForgeryFieldName}"" type=""hidden"" value=""([^""]+)"" \/\>");
    if (requestVerificationTokenMatch.Success)
    {
        return requestVerificationTokenMatch.Groups[1].Captures[0].Value;
    }

    throw new ArgumentException($"Anti forgery token '{AntiForgeryFieldName}' not found in HTML", nameof(htmlBody));
}

В этом методе мы используем регулярное выражение для извлечения элемента HTML из строки htmlBody, которая содержит значение поля защиты от подделки. Если выражение выполнено успешно, мы возвращаем его значение, в противном случае мы генерируем исключение.

Наконец, мы можем создать основной метод, который будет возвращать результаты обоих этих методов:

public static async Task<(string fieldValue, string cookieValue)> ExtractAntiForgeryValues(HttpResponseMessage response)
{
    var cookie = ExtractAntiForgeryCookieValueFrom(response);
    var token = ExtractAntiForgeryToken(await response.Content.ReadAsStringAsync());
    return (token, cookie);
}

Итак, мы просто вызываем оба метода, собираем их результаты и возвращаем их как объект Tuple.

Вот и все, теперь мы можем изменить наши методы тестирования и включить проверку AntiForgeryToken в контроллер.

Изменение методов тестирования

Мы собираемся изменить метод Create_SentWrongModel_ReturnsViewWithErrorMessages:

[Fact]
public async Task Create_SentWrongModel_ReturnsViewWithErrorMessages()
{
    var initResponse = await _client.GetAsync("/Employees/Create");
    var antiForgeryValues = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initResponse);
    var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Employees/Create");

    postRequest.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());

    var formModel = new Dictionary&lt;string, string&gt;
    {
        { AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
        { "Name", "New Employee" },
        { "Age", "25" }
    };

    postRequest.Content = new FormUrlEncodedContent(formModel);

    var response = await _client.SendAsync(postRequest);
    response.EnsureSuccessStatusCode();

    var responseString = await response.Content.ReadAsStringAsync();

    Assert.Contains("Account number is required", responseString);
}

Итак, мы должны сначала отправить запрос GET, чтобы получить ответ, который мы используем для извлечения наших значений для защиты от подделки запроса. После извлечения мы присваиваем значение cookie заголовку нашего запроса POST и указываем значение поля в объекте formModel.

Давайте посмотрим, как эти файлы cookie и значения полей будут выглядеть в ответе.

Сначала файл cookie из ответа:

Затем поле из тела HTML:

Мы видим, что и файл cookie, и поле имеют те же имена, которые мы объявили в классе TestingWebAppFactory.

Изменение дополнительного метода тестирования

Давайте внесем те же изменения в метод Create_WhenPOSTExecuted_ReturnsToIndexView:

[Fact]
public async Task Create_WhenPOSTExecuted_ReturnsToIndexView()
{
    var initResponse = await _client.GetAsync("/Employees/Create");
    var antiForgeryValues = await AntiForgeryTokenExtractor.ExtractAntiForgeryValues(initResponse);
    var postRequest = new HttpRequestMessage(HttpMethod.Post, "/Employees/Create");

    postRequest.Headers.Add("Cookie", new CookieHeaderValue(AntiForgeryTokenExtractor.AntiForgeryCookieName, antiForgeryValues.cookieValue).ToString());

    var modelData = new Dictionary&lt;string, string&gt;
    {
        { AntiForgeryTokenExtractor.AntiForgeryFieldName, antiForgeryValues.fieldValue },
        { "Name", "New Employee" },
        { "Age", "25" },
        { "AccountNumber", "214-5874986532-21" }
    };

    postRequest.Content = new FormUrlEncodedContent(modelData);

    var response = await _client.SendAsync(postRequest);
    response.EnsureSuccessStatusCode();

    var responseString = await response.Content.ReadAsStringAsync();

    Assert.Contains("New Employee", responseString);
    Assert.Contains("214-5874986532-21", responseString);
}

Кроме того, мы изменили имя метода и одну переменную в этом методе для лучшей читаемости. Все, что нам нужно сделать, это убедиться, что наши тесты проходят:

Работает.

Заключение

Из этой статьи мы узнали:

  • Как внедрить службу защиты от подделки в IServiceCollection
  • Способ извлечения файлов cookie и значений поля защиты от подделки из ответа.
  • Как изменить наши методы тестирования для работы с проверкой AntiForgeryToken

В следующей статье мы рассмотрим тестирование пользовательского интерфейса с помощью библиотеки Selenium.