подписка
This commit is contained in:
+189
@@ -0,0 +1,189 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user