using System.Text.Json; using Shouldly; using Tnsu.ApprovalRoute.Infrastructure.SharePoint; namespace Tnsu.ApprovalRoute.IntegrationTests.Infrastructure; /// /// Разбор ответов SharePoint REST — единственная часть SharePointClient, которую /// можно проверить без живого тенанта: сам HTTP-вызов неотделим от аутентификации /// по сертификату (ClientCertificateCredential ходит в Azure AD за токеном перед /// каждым запросом), а вот разбор уже полученного JSON — чистая функция. /// /// Это самое непроверенное место всей интеграции: форма ответа Search REST /// (_api/search/query) задокументирована, но подтвердить её точное соответствие /// без реального тенанта нельзя — только этими тестами на образцах ответа. /// 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(); } }