Как выполнить интеграционный тест на конечной точке контроллера API ASP.NET Core, требующей аутентификации и проверки тоC#

Место общения программистов C#
Ответить Пред. темаСлед. тема
Anonymous
 Как выполнить интеграционный тест на конечной точке контроллера API ASP.NET Core, требующей аутентификации и проверки то

Сообщение Anonymous »

Описание
У меня есть конечная точка контроллера API ASP.NET Core, для которой требуется:
  • аутентифицированный пользователь, и
  • проверка токенов защиты от подделки.
Я хочу выполнить интеграционный тест на этой конечной точке.
Проблема
Я не могу отправить запрос, в котором есть как аутентифицированный пользователь, так и необходимые токены защиты от подделки/файлы cookie и файлы cookie аутентификации. Таким образом, конечная точка продолжает возвращать неверный запрос, прежде чем достигнет обработчика.
Вопрос
Как вы выполняете интеграционный тест на конечная точка, которая требует как аутентификации, так и проверки токенов защиты от подделки?
Код
Чтобы помочь разобраться в этом вопросе, я создал пример приложения, демонстрирующий проблема, с которой я столкнулся.
Конечные точки контроллера API
В примере приложения есть контроллер API с четырьмя конечными точками POST:
1. Неаутентифицированная (анонимная) конечная точка – проверка на подделку НЕ требуется

Код: Выделить всё

[AllowAnonymous]
[HttpPost("Anonymous/{name}")]
public IActionResult AnonymousPost(string name)
{
return Ok(name);
}
2. Аутентифицированная конечная точка – проверка на подделку НЕ требуется

Код: Выделить всё

[HttpPost("Authenticated/{name}")]
public IActionResult AuthenticatedPost(string name)
{
return Ok(name);
}
3. Неаутентифицированная (анонимная) конечная точка – требуется проверка на подделку

Код: Выделить всё

[AllowAnonymous]
[ValidateAntiForgeryToken]
[HttpPost("Anonymous/Antiforgery/{name}")]
public IActionResult AnonymousAntiforgeryPost(string name)
{
return Ok(name);
}
4. Конечная точка, прошедшая проверку подлинности: требуется проверка на подделку

Код: Выделить всё

[ValidateAntiForgeryToken]
[HttpPost("Authenticated/Antiforgery/{name}")]
public IActionResult AuthenticatedAntiforgeryPost(string name)
{
return Ok(name);
}
Конечная точка № 4, для которой требуется аутентифицированный пользователь, а также проверка токенов защиты от подделки, — это конечная точка, которую я не могу успешно протестировать.
Аутентификация
Приложение использует аутентификацию с использованием файлов cookie и требует аутентификации пользователя.

Код: Выделить всё

// Add authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
}).AddCookie(IdentityConstants.ApplicationScheme, options =>
{
options.LoginPath = new PathString("/Login");
}).AddTwoFactorRememberMeCookie();

// Add authorization
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Тестовый проект настраивает тестовую схему аутентификации

Код: Выделить всё

public static IWebHostBuilder ConfigureTestAuthenticationScheme(this IWebHostBuilder builder, string scheme)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.ConfigureTestServices(services =>
{
services.AddAuthentication(defaultScheme: "TestScheme")
.AddScheme("TestScheme", options => { });
});
}
Где TestAuthHandler наследует от AuthenticatonHandler и переопределяет метод HandleAuthenticateAsync.

Код: Выделить всё

protected override Task HandleAuthenticateAsync()
{
Claim[] claims =
[
new Claim(ClaimTypes.Name, "testuser"),
new Claim(ClaimTypes.NameIdentifier, "testuser")
];
ClaimsIdentity identity = new (claims, "Test");
ClaimsPrincipal principal = new (identity);
AuthenticationTicket ticket = new (principal, "TestScheme");

AuthenticateResult result = AuthenticateResult.Success(ticket);

return Task.FromResult(result);
}
Аутентифицированный клиент может быть создан для теста следующим образом:

Код: Выделить всё

public HttpClient GetAuthenticatedClient(CookieContainerHandler? cookieHandler = default)
{
cookieHandler ??= new();

string testScheme = "TestScheme";

HttpClient client = WithWebHostBuilder(builder =>
{
builder.ConfigureTestAuthenticationScheme(testScheme);
})
.CreateDefaultClient(cookieHandler);

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(scheme: testScheme);

return client;
}
Защита от подделки
Атрибут ValidateAntiForgeryToken используется на двух конечных точках (#3 и #4), которые требуют проверки токена защиты от подделки.< /p>
Тестовый проект добавляет AntiforgeryController в IWebHostBuilder, который возвращает объект JSON, содержащий действительные токены защиты от подделки.

Код: Выделить всё

public static IWebHostBuilder ConfigureAntiforgeryTokenResource(this IWebHostBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.ConfigureTestServices((services) =>
{
services.AddControllers()
.AddApplicationPart(typeof(AntiforgeryTokenController).Assembly);
});
}

Код: Выделить всё

public IActionResult GetAntiforgeryTokens(
[FromServices] IAntiforgery antiforgery,
[FromServices] IOptions  options)
{
ArgumentNullException.ThrowIfNull(antiforgery);
ArgumentNullException.ThrowIfNull(options);

AntiforgeryTokenSet tokens = antiforgery.GetTokens(HttpContext);

AntiforgeryTokens model = new()
{
CookieName = options.Value!.Cookie!.Name!,
CookieValue = tokens.CookieToken!,
FormFieldName = options.Value.FormFieldName,
HeaderName = tokens.HeaderName!,
RequestToken = tokens.RequestToken!
};

return Json(model);
}
CustomWebApplicationFactory предоставляет метод GetAntiForgeryTokensAsync для проверки связи с AntiforgeryTokenController в тестовом методе.

Код: Выделить всё

public async Task GetAntiforgeryTokensAsync(
Func? httpClientFactory = null,
CancellationToken cancellationToken = default)
{
using HttpClient httpClient = httpClientFactory?.Invoke() ?? CreateDefaultClient();

AntiforgeryTokens? tokens = await httpClient.GetFromJsonAsync(
AntiforgeryTokenController.GetTokensUri,
cancellationToken);

return tokens!;
}
Интеграционные тесты
Я могу успешно протестировать первые три конечные точки, однако, когда мне нужно протестировать четвертую конечную точку, требуется как аутентифицированный при проверке пользователя и токена защиты от подделки возвращается неверный запрос.
1. Тестирование неаутентифицированной (анонимной) конечной точки — проверка на подделку НЕ требуется

Код: Выделить всё

public async Task Unauthenticated_request_to_anonymous_endpoint_returns_ok()
{
// Arrange
HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/anonymous/testname", UriKind.Relative)
};

// Act
HttpResponseMessage response = await _client.SendAsync(message);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
2. Тестирование конечной точки, прошедшей проверку подлинности: проверка на подделку НЕ требуется

Код: Выделить всё

public async Task Authenticated_request_to_autheticated_endpoint_returns_ok()
{
// Arrange
HttpClient client = _factory.GetAuthenticatedClient();

HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/authenticated/testname", UriKind.Relative)
};

// Act
HttpResponseMessage response = await client.SendAsync(message);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
3. Тестирование неаутентифицированной (анонимной) конечной точки – требуется проверка на подделку

Код: Выделить всё

public async Task Unauthenticated_request_to_anonymous_antiforgery_endpoint_with_tokens_returns_ok()
{
// Arrange
AntiforgeryTokens tokens = await _factory.GetAntiforgeryTokensAsync();

CookieContainerHandler cookieHandler = new();
cookieHandler.Container.Add(
_factory.Server.BaseAddress,
new Cookie(tokens.CookieName, tokens.CookieValue));

HttpClient client = _factory.CreateDefaultClient(cookieHandler);

client.DefaultRequestHeaders.Add(tokens.HeaderName, tokens.RequestToken);

HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/anonymous/antiforgery/testname", UriKind.Relative)
};

// Act
HttpResponseMessage response = await client.SendAsync(message);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
4. Тестирование конечной точки с аутентификацией — требуется проверка на защиту от подделки
Я читал, что браузер автоматически извлекает все файлы cookie из заголовков ответов сервера и присоединяет их к следующему запросу. Чтобы проверка с помощью CSRF прошла успешно, ее необходимо смоделировать. Поэтому этот тест вызывает метод GetAuthenticationCookies, который выполняет вход в приложение и извлекает файлы cookie аутентификации из ответа.

Код: Выделить всё

public async Task GetAuthenticationCookies(CookieContainerHandler cookieHandler, AntiforgeryTokens tokens)
{
CancellationToken cancellationToken = new CancellationTokenSource().Token;

HttpClient client = _factory.CreateDefaultClient(cookieHandler);

Uri uri = new($"{client.BaseAddress!.AbsoluteUri}login");

Dictionary postData = new()
{
{ "Input.UserName", "testuser" },
{ "Input.Password", "password" },
{ tokens!.FormFieldName, tokens.RequestToken }
};

HttpContent formContent = new FormUrlEncodedContent(postData);

HttpResponseMessage response = await client.PostAsync(uri, formContent, cancellationToken);

return response.Headers.GetValues("Set-Cookie").ToList();
}
Затем тест добавляет эти файлы cookie в заголовки запросов клиентов в дополнение к токенам защиты от подделки.

Код: Выделить всё

public async Task Authenticated_request_to_authenticated_antiforgery_endpoint_with_tokens_returns_ok()
{
// Arrange
AntiforgeryTokens tokens = await _factory.GetAntiforgeryTokensAsync();

CookieContainerHandler cookieHandler = new();
cookieHandler.Container.Add(
_factory.Server.BaseAddress,
new Cookie(tokens.CookieName, tokens.CookieValue));

HttpClient client = _factory.GetAuthenticatedClient(cookieHandler);

List cookies = await GetAuthenticationCookies(cookieHandler, tokens);

client.DefaultRequestHeaders.Add(tokens.HeaderName, tokens.RequestToken);
client.DefaultRequestHeaders.Add("Cookie", cookies);

HttpRequestMessage message = new()
{
Method = HttpMethod.Post,
RequestUri = new Uri("/api/authenticated/antiforgery/testname", UriKind.Relative)
};

// Act
HttpResponseMessage response = await client.SendAsync(message);

// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
К сожалению, этот тест возвращает неверный запрос и никогда не достигает обработчика.


Подробнее здесь: https://stackoverflow.com/questions/787 ... t-that-req
Реклама
Ответить Пред. темаСлед. тема

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение

Вернуться в «C#»