заработало

This commit is contained in:
gorbenko.pavel
2026-08-12 17:07:21 +05:00
parent 0b2a05cd94
commit 6daecd2e18
6 changed files with 17 additions and 277 deletions
@@ -77,28 +77,15 @@ public sealed class ReminderServiceTests : IClassFixture<PostgresDatabaseFixture
.Setup(x => x.GetListTitleAsync(SiteUrl, listUrl, It.IsAny<CancellationToken>()))
.ReturnsAsync(DataListTitle);
sharePoint
.Setup(x => x.GetListServerRelativeUrlAsync(SiteUrl, ApprovalListTitle, It.IsAny<CancellationToken>()))
.ReturnsAsync("/sites/requests/Lists/ApprovalListRelUrl");
// Search сужает набор до активных строк (На согласовании/На исполнении) —
// мок отвечает так же, как реальный Search: по статусу строки, не по дате.
// Активные строки сервис отбирает прямым $filter по ApprovalStatus — мок
// отвечает так же: по значению статуса, а не по признаку решённости.
var activeRows = allRows
.Where(r => (string)r.GetType().GetProperty("ApprovalStatus")!.GetValue(r)! is
"На согласовании" or "На исполнении")
.ToList();
sharePoint
.Setup(x => x.SearchListItemIdsAsync(
SiteUrl, It.Is<string>(q => q.Contains("ApprovalStatusOWSCHCS")), It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(activeRows.Select(r => (int)r.GetType().GetProperty("Id")!.GetValue(r)!).ToList());
// Дочитывание полных полей найденных Search'ем строк — по Id. Матчер ищет
// "filter=Id eq", а не просто "Id eq" — иначе он совпал бы и с
// "TicketId eq" (та подстрока в нём есть буквально), и два setup'а
// конфликтовали бы между собой.
sharePoint
.Setup(x => x.GetItemsByListTitleAsync(
SiteUrl, ApprovalListTitle, It.Is<string>(q => q.Contains("filter=Id eq")), It.IsAny<CancellationToken>()))
SiteUrl, ApprovalListTitle, It.Is<string>(q => q.Contains("ApprovalStatus eq")), It.IsAny<CancellationToken>()))
.ReturnsAsync(activeRows.Select(Json).ToList());
sharePoint
@@ -1,99 +0,0 @@
using System.Text.Json;
using Shouldly;
using Tnsu.ApprovalRoute.Infrastructure.SharePoint;
namespace Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
/// <summary>
/// Разбор ответов SharePoint REST — единственная часть SharePointClient, которую
/// можно проверить без живого тенанта: сам HTTP-вызов неотделим от аутентификации
/// по сертификату (ClientCertificateCredential ходит в Azure AD за токеном перед
/// каждым запросом), а вот разбор уже полученного JSON — чистая функция.
///
/// Это самое непроверенное место всей интеграции: форма ответа Search REST
/// (_api/search/query) задокументирована, но подтвердить её точное соответствие
/// без реального тенанта нельзя — только этими тестами на образцах ответа.
/// </summary>
public sealed class SharePointClientParsingTests
{
private static JsonElement Parse(string json) => JsonDocument.Parse(json).RootElement;
[Fact(DisplayName = "Search: с odata=nometadata Rows/Cells приходят голыми массивами")]
public void ParseSearchListItemIds_BareArrays_ReturnsIds()
{
const string response = """
{
"PrimaryQueryResult": {
"RelevantResults": {
"RowCount": 2,
"Table": {
"Rows": [
{ "Cells": [ { "Key": "Rank", "Value": "1" }, { "Key": "ListItemID", "Value": "11" } ] },
{ "Cells": [ { "Key": "ListItemID", "Value": "27" }, { "Key": "Rank", "Value": "2" } ] }
]
}
}
}
}
""";
var ids = SharePointClient.ParseSearchListItemIds(Parse(response));
ids.ShouldBe([11, 27]);
}
[Fact(DisplayName = "Search: без odata=nometadata Rows/Cells завёрнуты в {results: [...]}")]
public void ParseSearchListItemIds_WrappedResults_ReturnsIds()
{
const string response = """
{
"PrimaryQueryResult": {
"RelevantResults": {
"Table": {
"Rows": {
"results": [
{ "Cells": { "results": [ { "Key": "ListItemID", "Value": "42" } ] } }
]
}
}
}
}
}
""";
var ids = SharePointClient.ParseSearchListItemIds(Parse(response));
ids.ShouldBe([42]);
}
[Fact(DisplayName = "Search: нет совпадений — пустой список, а не исключение")]
public void ParseSearchListItemIds_NoRows_ReturnsEmpty()
{
const string response = """{ "PrimaryQueryResult": { "RelevantResults": { "RowCount": 0, "Table": { "Rows": [] } } } } """;
SharePointClient.ParseSearchListItemIds(Parse(response)).ShouldBeEmpty();
}
[Fact(DisplayName = "Search: неожиданная форма ответа не роняет разбор")]
public void ParseSearchListItemIds_UnexpectedShape_ReturnsEmpty()
{
SharePointClient.ParseSearchListItemIds(Parse("""{ "odata.error": "что-то не так" }""")).ShouldBeEmpty();
}
[Fact(DisplayName = "RootFolder/ServerRelativeUrl разбирается для условия Path в KQL")]
public void ParseServerRelativeUrl_ReturnsValue()
{
const string response = """
{ "RootFolder": { "ServerRelativeUrl": "/sites/requests/Lists/Заявка тест — согласование" } }
""";
SharePointClient.ParseServerRelativeUrl(Parse(response))
.ShouldBe("/sites/requests/Lists/Заявка тест — согласование");
}
[Fact(DisplayName = "Список без RootFolder в ответе — null, а не исключение")]
public void ParseServerRelativeUrl_Missing_ReturnsNull()
{
SharePointClient.ParseServerRelativeUrl(Parse("{}")).ShouldBeNull();
}
}