Files
tnsu-approval-route/backend/tests/Tnsu.ApprovalRoute.IntegrationTests/ApplicationServices/SharePointWebhookSubscriberTests.cs
T
gorbenko.pavel 82512eb1db
Tests / frontend-test (push) Successful in 44s
Tests / backend-test (push) Failing after 17m4s
Архивация списков (#13)
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/13
2026-08-19 09:08:36 +00:00

190 lines
8.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Shouldly;
using Tnsu.ApprovalRoute.Infrastructure.SharePoint;
using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
namespace Tnsu.ApprovalRoute.IntegrationTests.ApplicationServices;
/// <summary>
/// Подписка нового списка данных на уведомления SharePoint (POST /Webhook/Subscribe).
///
/// Проверяется ровно то, что нельзя увидеть по типам: как складывается строка
/// запроса (ручка принимает всё параметрами, тела нет) и что сбой портала не рушит
/// провижининг — подписка следствие создания списка, а не его условие.
/// </summary>
public sealed class SharePointWebhookSubscriberTests
{
private const string SiteUrl = "https://tnsukz.sharepoint.com/sites/requests";
private const string ListTitle = "Обходной лист 3 — данные";
/// <summary>Перехватывает запрос вместо сети: запоминает его и отдаёт заданный ответ.</summary>
private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> respond) : HttpMessageHandler
{
public HttpRequestMessage? Captured { get; private set; }
public int CallCount { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
Captured = request;
CallCount++;
return Task.FromResult(respond(request));
}
}
// Токен вызывающего есть — тогда авторизатор пробрасывает его и не идёт за
// client_credentials (у которого в тестах нет ни секрета, ни сети).
// fromHttpContext=false — путь фоновой задачи: токен положен явно, HttpContext'а
// нет вовсе (см. TnsuPortalCallerToken.Set / CreateNextListPartitionJob).
private static TnsuPortalAuthorizer BuildAuthorizer(bool fromHttpContext = true)
{
var accessor = new HttpContextAccessor();
var callerToken = new TnsuPortalCallerToken(accessor);
if (fromHttpContext)
{
var context = new DefaultHttpContext();
context.Request.Headers.Authorization = "Bearer test-token";
accessor.HttpContext = context;
}
else
{
callerToken.Set("Bearer test-token");
}
return new TnsuPortalAuthorizer(
callerToken,
new TnsuPortalTokenProvider(new HttpClient(), Options.Create(new TnsuPortalOptions())));
}
private static (HttpSharePointWebhookSubscriber Subscriber, StubHandler Handler) Build(
Func<HttpRequestMessage, HttpResponseMessage> respond,
SharePointOptions? options = null,
bool tokenFromHttpContext = true)
{
var handler = new StubHandler(respond);
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://api.tnsu.kz") };
var subscriber = new HttpSharePointWebhookSubscriber(
httpClient,
BuildAuthorizer(tokenFromHttpContext),
Options.Create(options ?? new SharePointOptions()),
NullLogger<HttpSharePointWebhookSubscriber>.Instance);
return (subscriber, handler);
}
private static HttpResponseMessage Ok() => new(System.Net.HttpStatusCode.OK);
[Fact(DisplayName = "Подписка уходит POST'ом со всеми тремя параметрами в строке запроса")]
public async Task TrySubscribeAsync_SendsPostWithQueryParameters()
{
// Arrange
var (subscriber, handler) = Build(_ => Ok());
// Action
var subscribed = await subscriber.TrySubscribeAsync(SiteUrl, ListTitle, CancellationToken.None);
// Assert
subscribed.ShouldBeTrue();
handler.Captured!.Method.ShouldBe(HttpMethod.Post);
handler.Captured.Content.ShouldBeNull(); // тела у ручки нет
var query = Uri.UnescapeDataString(handler.Captured.RequestUri!.Query);
handler.Captured.RequestUri.AbsolutePath.ShouldBe("/Webhook/Subscribe");
query.ShouldContain($"listName={ListTitle}");
query.ShouldContain("notificationUrl=https://api.tnsu.kz/Webhook/SharePoint");
// Токен из входящего запроса пробрасывается как есть (см. TnsuPortalAuthorizer).
handler.Captured.Headers.Authorization?.ToString().ShouldBe("Bearer test-token");
}
[Fact(DisplayName = "В url уходит ПОЛНЫЙ адрес сайта, а не короткое имя")]
public async Task TrySubscribeAsync_SendsFullSiteUrl()
{
// Arrange
var (subscriber, handler) = Build(_ => Ok());
// Action
await subscriber.TrySubscribeAsync(SiteUrl, ListTitle, CancellationToken.None);
// Assert
var query = Uri.UnescapeDataString(handler.Captured!.RequestUri!.Query);
query.ShouldContain($"url={SiteUrl}");
}
[Fact(DisplayName = "Токен фоновой задачи (без HttpContext) тоже уходит в заголовок")]
public async Task TrySubscribeAsync_TokenSetExplicitly_IsSentAsAuthorization()
{
// Arrange: так работает провижининг — HttpContext'а нет, токен передан
// аргументом задачи (см. CreateNextListPartitionJob).
var (subscriber, handler) = Build(_ => Ok(), tokenFromHttpContext: false);
// Action
var subscribed = await subscriber.TrySubscribeAsync(SiteUrl, ListTitle, CancellationToken.None);
// Assert
subscribed.ShouldBeTrue();
handler.Captured!.Headers.Authorization?.ToString().ShouldBe("Bearer test-token");
}
[Fact(DisplayName = "Отказ портала не бросает исключение — провижининг списка не рушится")]
public async Task TrySubscribeAsync_PortalRejects_ReturnsFalseWithoutThrowing()
{
// Arrange
var (subscriber, _) = Build(_ => new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized)
{
Content = new StringContent("no token")
});
// Action
var subscribed = await subscriber.TrySubscribeAsync(SiteUrl, ListTitle, CancellationToken.None);
// Assert
subscribed.ShouldBeFalse();
}
[Fact(DisplayName = "Недоступный портал (исключение в сети) тоже не бросает наружу")]
public async Task TrySubscribeAsync_NetworkFailure_ReturnsFalseWithoutThrowing()
{
// Arrange
var (subscriber, _) = Build(_ => throw new HttpRequestException("портал недоступен"));
// Action
var subscribed = await subscriber.TrySubscribeAsync(SiteUrl, ListTitle, CancellationToken.None);
// Assert
subscribed.ShouldBeFalse();
}
[Fact(DisplayName = "NotificationUrl не задан — запрос не отправляется вовсе")]
public async Task TrySubscribeAsync_NoNotificationUrl_DoesNotCallPortal()
{
// Arrange: пустой адрес приёмника — единственный выключатель подписки
// (отдельного флага Enabled нет, см. SharePointOptions.NotificationUrl).
var (subscriber, handler) = Build(_ => Ok(), new SharePointOptions { NotificationUrl = "" });
// Action
var subscribed = await subscriber.TrySubscribeAsync(SiteUrl, ListTitle, CancellationToken.None);
// Assert
subscribed.ShouldBeFalse();
handler.CallCount.ShouldBe(0);
}
[Fact(DisplayName = "Пустой заголовок списка — подписывать нечего, запроса нет")]
public async Task TrySubscribeAsync_EmptyListTitle_DoesNotCallPortal()
{
// Arrange
var (subscriber, handler) = Build(_ => Ok());
// Action
var subscribed = await subscriber.TrySubscribeAsync(SiteUrl, " ", CancellationToken.None);
// Assert
subscribed.ShouldBeFalse();
handler.CallCount.ShouldBe(0);
}
}