основное сделано

This commit is contained in:
gorbenko.pavel
2026-08-12 15:12:11 +05:00
parent 4b83d666f1
commit 9fc089c37a
47 changed files with 2481 additions and 52 deletions
@@ -0,0 +1,99 @@
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();
}
}