В предыдущей статье, мы узнали, как писать интеграционные тесты для различных действий (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<string, string>
{
{ 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<string, string>
{
{ 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.