From 16ac57e5173de0baa46a74beeea05fb09d5f3180 Mon Sep 17 00:00:00 2001 From: Volodymyr Dvernytskyi Date: Fri, 20 Jun 2025 13:14:27 +0300 Subject: [PATCH 1/6] Pagination support for MicrosoftGraph module --- .../App/MicrosoftGraph/app.json | 2 +- .../src/GraphClient.Codeunit.al | 46 ++++++++ .../src/GraphClientImpl.Codeunit.al | 59 ++++++++++ .../src/GraphPaginationData.Codeunit.al | 80 ++++++++++++++ .../src/GraphPaginationDataImpl.Codeunit.al | 59 ++++++++++ .../helper/GraphPaginationHelper.Codeunit.al | 101 ++++++++++++++++++ .../src/helper/GraphRequestHelper.Codeunit.al | 5 + 7 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 src/System Application/App/MicrosoftGraph/src/GraphPaginationData.Codeunit.al create mode 100644 src/System Application/App/MicrosoftGraph/src/GraphPaginationDataImpl.Codeunit.al create mode 100644 src/System Application/App/MicrosoftGraph/src/helper/GraphPaginationHelper.Codeunit.al diff --git a/src/System Application/App/MicrosoftGraph/app.json b/src/System Application/App/MicrosoftGraph/app.json index bd22a00b21..8f08ee299c 100644 --- a/src/System Application/App/MicrosoftGraph/app.json +++ b/src/System Application/App/MicrosoftGraph/app.json @@ -54,7 +54,7 @@ "idRanges": [ { "from": 9350, - "to": 9359 + "to": 9361 } ], "target": "OnPrem", diff --git a/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al index 725be69c6b..7e1fd60be2 100644 --- a/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al @@ -139,4 +139,50 @@ codeunit 9350 "Graph Client" begin exit(GraphClientImpl.Delete(RelativeUriToResource, GraphOptionalParameters, HttpResponseMessage)); end; + + #region Pagination Support + + /// + /// Get any request to the microsoft graph API with pagination support + /// + /// Does not require UI interaction. This method handles pagination automatically. + /// A relativ uri including the resource segments + /// A wrapper for optional header and query parameters + /// The pagination data object to track pagination state + /// The response message object. + /// True if the operation was successful; otherwise - false. + /// Authentication failed. + procedure GetWithPagination(RelativeUriToResource: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var GraphPaginationData: Codeunit "Graph Pagination Data"; var HttpResponseMessage: Codeunit "Http Response Message"): Boolean + begin + exit(GraphClientImpl.GetWithPagination(RelativeUriToResource, GraphOptionalParameters, GraphPaginationData, HttpResponseMessage)); + end; + + /// + /// Get the next page of results using pagination data + /// + /// Does not require UI interaction. + /// The pagination data object containing the next link + /// The response message object. + /// True if the operation was successful; otherwise - false. + /// Authentication failed. + procedure GetNextPage(var GraphPaginationData: Codeunit "Graph Pagination Data"; var HttpResponseMessage: Codeunit "Http Response Message"): Boolean + begin + exit(GraphClientImpl.GetNextPage(GraphPaginationData, HttpResponseMessage)); + end; + + /// + /// Get all pages of results automatically + /// + /// Does not require UI interaction. This method fetches all pages automatically and returns the combined results. + /// A relativ uri including the resource segments + /// A wrapper for optional header and query parameters + /// A JSON array containing all results from all pages + /// True if the operation was successful; otherwise - false. + /// Authentication failed. + procedure GetAllPages(RelativeUriToResource: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var JsonResults: JsonArray): Boolean + begin + exit(GraphClientImpl.GetAllPages(RelativeUriToResource, GraphOptionalParameters, JsonResults)); + end; + + #endregion } \ No newline at end of file diff --git a/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al index 846ced7919..49be2b181c 100644 --- a/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al @@ -99,5 +99,64 @@ codeunit 9351 "Graph Client Impl." exit(HttpResponseMessage.GetIsSuccessStatusCode()); end; + procedure GetWithPagination(RelativeUriToResource: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var GraphPaginationData: Codeunit "Graph Pagination Data"; var HttpResponseMessage: Codeunit "Http Response Message"): Boolean + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + begin + // Apply page size if set + GraphPaginationHelper.ApplyPageSize(GraphOptionalParameters, GraphPaginationData); + + // Make the request + if not Get(RelativeUriToResource, GraphOptionalParameters, HttpResponseMessage) then + exit(false); + + // Extract pagination data + GraphPaginationHelper.ExtractNextLink(HttpResponseMessage, GraphPaginationData); + exit(HttpResponseMessage.GetIsSuccessStatusCode()); + end; + + procedure GetNextPage(var GraphPaginationData: Codeunit "Graph Pagination Data"; var HttpResponseMessage: Codeunit "Http Response Message"): Boolean + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + NextLink: Text; + begin + NextLink := GraphPaginationData.GetNextLink(); + + if NextLink = '' then + exit(false); + + GraphRequestHelper.SetRestClient(RestClient); + HttpResponseMessage := GraphRequestHelper.GetByFullUrl(NextLink); + + // Update pagination data + GraphPaginationHelper.ExtractNextLink(HttpResponseMessage, GraphPaginationData); + exit(HttpResponseMessage.GetIsSuccessStatusCode()); + end; + + procedure GetAllPages(RelativeUriToResource: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var JsonResults: JsonArray): Boolean + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + IterationCount: Integer; + begin + // First request with pagination + if not GetWithPagination(RelativeUriToResource, GraphOptionalParameters, GraphPaginationData, HttpResponseMessage) then + exit(false); + + // Process first page + GraphPaginationHelper.CombineValueArrays(HttpResponseMessage, JsonResults); + + // Fetch remaining pages + while GraphPaginationData.HasMorePages() and GraphPaginationHelper.IsWithinIterationLimit(IterationCount, GraphPaginationHelper.GetMaxIterations()) do begin + if not GetNextPage(GraphPaginationData, HttpResponseMessage) then + exit(false); + + GraphPaginationHelper.CombineValueArrays(HttpResponseMessage, JsonResults); + end; + + exit(true); + end; + } diff --git a/src/System Application/App/MicrosoftGraph/src/GraphPaginationData.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/GraphPaginationData.Codeunit.al new file mode 100644 index 0000000000..2ac4533e81 --- /dev/null +++ b/src/System Application/App/MicrosoftGraph/src/GraphPaginationData.Codeunit.al @@ -0,0 +1,80 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Integration.Graph; + +/// +/// Holder for pagination data when working with Microsoft Graph API responses. +/// +codeunit 9360 "Graph Pagination Data" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + var + GraphPaginationDataImpl: Codeunit "Graph Pagination Data Impl."; + + /// + /// Sets the next link URL for retrieving the next page of results. + /// + /// The @odata.nextLink value from the Graph response. + procedure SetNextLink(NewNextLink: Text) + begin + GraphPaginationDataImpl.SetNextLink(NewNextLink); + end; + + /// + /// Gets the current next link URL. + /// + /// The URL to retrieve the next page of results. + procedure GetNextLink(): Text + begin + exit(GraphPaginationDataImpl.GetNextLink()); + end; + + /// + /// Checks if there are more pages available. + /// + /// True if more pages are available; otherwise false. + procedure HasMorePages(): Boolean + begin + exit(GraphPaginationDataImpl.HasMorePages()); + end; + + /// + /// Sets the page size for pagination requests. + /// + /// The number of items to retrieve per page (max 999). + procedure SetPageSize(NewPageSize: Integer) + begin + GraphPaginationDataImpl.SetPageSize(NewPageSize); + end; + + /// + /// Gets the current page size. + /// + /// The number of items per page. + procedure GetPageSize(): Integer + begin + exit(GraphPaginationDataImpl.GetPageSize()); + end; + + /// + /// Gets the default page size. + /// + /// The default number of items per page. + procedure GetDefaultPageSize(): Integer + begin + exit(GraphPaginationDataImpl.GetDefaultPageSize()); + end; + + /// + /// Resets the pagination data to initial state. + /// + procedure Reset() + begin + GraphPaginationDataImpl.Reset(); + end; +} \ No newline at end of file diff --git a/src/System Application/App/MicrosoftGraph/src/GraphPaginationDataImpl.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/GraphPaginationDataImpl.Codeunit.al new file mode 100644 index 0000000000..dee2b87ce8 --- /dev/null +++ b/src/System Application/App/MicrosoftGraph/src/GraphPaginationDataImpl.Codeunit.al @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Integration.Graph; + +codeunit 9361 "Graph Pagination Data Impl." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + NextLink: Text; + PageSize: Integer; + DefaultPageSizeErr: Label 'Page size must be between 1 and 999.'; + + procedure SetNextLink(NewNextLink: Text) + begin + NextLink := NewNextLink; + end; + + procedure GetNextLink(): Text + begin + exit(NextLink); + end; + + procedure HasMorePages(): Boolean + begin + exit(NextLink <> ''); + end; + + procedure SetPageSize(NewPageSize: Integer) + begin + if not (NewPageSize in [1 .. 999]) then + Error(DefaultPageSizeErr); + + PageSize := NewPageSize; + end; + + procedure GetPageSize(): Integer + begin + if PageSize = 0 then + exit(GetDefaultPageSize()); + + exit(PageSize); + end; + + procedure Reset() + begin + Clear(NextLink); + Clear(PageSize); + end; + + procedure GetDefaultPageSize(): Integer + begin + exit(100); + end; +} \ No newline at end of file diff --git a/src/System Application/App/MicrosoftGraph/src/helper/GraphPaginationHelper.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/helper/GraphPaginationHelper.Codeunit.al new file mode 100644 index 0000000000..0b10fac55b --- /dev/null +++ b/src/System Application/App/MicrosoftGraph/src/helper/GraphPaginationHelper.Codeunit.al @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Integration.Graph; + +using System.RestClient; + +codeunit 9359 "Graph Pagination Helper" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure ExtractNextLink(HttpResponseMessage: Codeunit "Http Response Message"; var GraphPaginationData: Codeunit "Graph Pagination Data") + var + ResponseJson: JsonObject; + JsonToken: JsonToken; + NextLink: Text; + ResponseText: Text; + begin + if not HttpResponseMessage.GetIsSuccessStatusCode() then begin + GraphPaginationData.SetNextLink(''); + exit; + end; + + ResponseText := HttpResponseMessage.GetContent().AsText(); + + // Parse JSON response + if not ResponseJson.ReadFrom(ResponseText) then begin + GraphPaginationData.SetNextLink(''); + exit; + end; + + // Extract nextLink + if ResponseJson.Get('@odata.nextLink', JsonToken) then + NextLink := JsonToken.AsValue().AsText(); + + GraphPaginationData.SetNextLink(NextLink); + end; + + procedure ExtractValueArray(HttpResponseMessage: Codeunit "Http Response Message"; var ValueArray: JsonArray): Boolean + var + ResponseJson: JsonObject; + JsonToken: JsonToken; + ResponseText: Text; + begin + Clear(ValueArray); + + if not HttpResponseMessage.GetIsSuccessStatusCode() then + exit(false); + + ResponseText := HttpResponseMessage.GetContent().AsText(); + + // Parse JSON response + if not ResponseJson.ReadFrom(ResponseText) then + exit(false); + + // Extract value array + if not ResponseJson.Get('value', JsonToken) then + exit(false); + + ValueArray := JsonToken.AsArray(); + exit(true); + end; + + procedure ApplyPageSize(var GraphOptionalParameters: Codeunit "Graph Optional Parameters"; GraphPaginationData: Codeunit "Graph Pagination Data") + begin + if GraphPaginationData.GetPageSize() > 0 then + GraphOptionalParameters.SetODataQueryParameter(Enum::"Graph OData Query Parameter"::top, Format(GraphPaginationData.GetPageSize())); + end; + + procedure CombineValueArrays(HttpResponseMessage: Codeunit "Http Response Message"; var JsonResults: JsonArray): Boolean + var + ValueArray: JsonArray; + JsonItem: JsonToken; + begin + if not ExtractValueArray(HttpResponseMessage, ValueArray) then + exit(false); + + foreach JsonItem in ValueArray do + JsonResults.Add(JsonItem); + + exit(true); + end; + + procedure IsWithinIterationLimit(var IterationCount: Integer; MaxIterations: Integer): Boolean + begin + if IterationCount >= MaxIterations then + exit(false); + + IterationCount += 1; + + exit(true); + end; + + procedure GetMaxIterations(): Integer + begin + exit(1000); // Safety limit to prevent infinite loops + end; +} \ No newline at end of file diff --git a/src/System Application/App/MicrosoftGraph/src/helper/GraphRequestHelper.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/helper/GraphRequestHelper.Codeunit.al index d4a137f238..5f15b122f3 100644 --- a/src/System Application/App/MicrosoftGraph/src/helper/GraphRequestHelper.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/helper/GraphRequestHelper.Codeunit.al @@ -62,4 +62,9 @@ codeunit 9354 "Graph Request Helper" PrepareRestClient(GraphOptionalParameters); HttpResponseMessage := RestClient.Send(HttpMethod, GraphUriBuilder.GetUri(), HttpContent); end; + + procedure GetByFullUrl(FullUrl: Text) HttpResponseMessage: Codeunit "Http Response Message" + begin + HttpResponseMessage := RestClient.Get(FullUrl); + end; } \ No newline at end of file From 19cf4fa54f9d0ee55894cfdbef31885b1f04953a Mon Sep 17 00:00:00 2001 From: Volodymyr Dvernytskyi Date: Fri, 11 Jul 2025 05:40:21 +0300 Subject: [PATCH 2/6] Return last HttpResponseMessage in GetAllPages for diagnosis Add tests for pagination methods and codeunits --- .../src/GraphClient.Codeunit.al | 5 +- .../src/GraphClientImpl.Codeunit.al | 3 +- .../src/GraphClientTest.Codeunit.al | 232 +++++++++++++ .../src/GraphPaginationDataTest.Codeunit.al | 143 ++++++++ .../src/GraphPaginationHelperTest.Codeunit.al | 218 ++++++++++++ .../src/GraphPaginationIntegTest.Codeunit.al | 314 ++++++++++++++++++ .../MockHttpClientHandlerMulti.Codeunit.al | 85 +++++ 7 files changed, 996 insertions(+), 4 deletions(-) create mode 100644 src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al create mode 100644 src/System Application/Test/MicrosoftGraph/src/GraphPaginationHelperTest.Codeunit.al create mode 100644 src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al create mode 100644 src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al diff --git a/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al index 7e1fd60be2..9f620a0ecb 100644 --- a/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al @@ -176,12 +176,13 @@ codeunit 9350 "Graph Client" /// Does not require UI interaction. This method fetches all pages automatically and returns the combined results. /// A relativ uri including the resource segments /// A wrapper for optional header and query parameters + /// The last response message object. /// A JSON array containing all results from all pages /// True if the operation was successful; otherwise - false. /// Authentication failed. - procedure GetAllPages(RelativeUriToResource: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var JsonResults: JsonArray): Boolean + procedure GetAllPages(RelativeUriToResource: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var HttpResponseMessage: Codeunit "Http Response Message"; var JsonResults: JsonArray): Boolean begin - exit(GraphClientImpl.GetAllPages(RelativeUriToResource, GraphOptionalParameters, JsonResults)); + exit(GraphClientImpl.GetAllPages(RelativeUriToResource, GraphOptionalParameters, HttpResponseMessage, JsonResults)); end; #endregion diff --git a/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al index 49be2b181c..f73b14f770 100644 --- a/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al @@ -133,11 +133,10 @@ codeunit 9351 "Graph Client Impl." exit(HttpResponseMessage.GetIsSuccessStatusCode()); end; - procedure GetAllPages(RelativeUriToResource: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var JsonResults: JsonArray): Boolean + procedure GetAllPages(RelativeUriToResource: Text; GraphOptionalParameters: Codeunit "Graph Optional Parameters"; var HttpResponseMessage: Codeunit "Http Response Message"; var JsonResults: JsonArray): Boolean var GraphPaginationHelper: Codeunit "Graph Pagination Helper"; GraphPaginationData: Codeunit "Graph Pagination Data"; - HttpResponseMessage: Codeunit "Http Response Message"; IterationCount: Integer; begin // First request with pagination diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al index 72db540ee9..9f63faac49 100644 --- a/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al +++ b/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al @@ -126,6 +126,181 @@ codeunit 135140 "Graph Client Test" LibraryAssert.AreEqual('HR Taskforce (ÄÖÜßäöü)', DisplayNameJsonToken.AsValue().AsText(), 'Incorrect Displayname.'); end; + [Test] + procedure GetWithPaginationSinglePageTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler"; + MockHttpContent: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + Success: Boolean; + begin + // [GIVEN] Mock response with no next link (single page) + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetSinglePageResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpClientHandler.SetResponse(MockHttpResponseMessage); + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [GIVEN] Set page size + GraphPaginationData.SetPageSize(50); + + // [WHEN] GetWithPagination is called + Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); + + // [THEN] Should be successful + LibraryAssert.AreEqual(true, Success, 'GetWithPagination should succeed'); + LibraryAssert.AreEqual(200, HttpResponseMessage.GetHttpStatusCode(), 'Should return 200 status'); + + // [THEN] Should have no more pages + LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages'); + end; + + [Test] + procedure GetWithPaginationMultiplePagesTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler"; + MockHttpContent: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + Success: Boolean; + begin + // [GIVEN] Mock response with next link (multiple pages) + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetMultiPageResponsePage1()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpClientHandler.SetResponse(MockHttpResponseMessage); + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [WHEN] GetWithPagination is called + Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); + + // [THEN] Should be successful and have more pages + LibraryAssert.AreEqual(true, Success, 'GetWithPagination should succeed'); + LibraryAssert.AreEqual(true, GraphPaginationData.HasMorePages(), 'Should have more pages'); + LibraryAssert.AreNotEqual('', GraphPaginationData.GetNextLink(), 'Should have next link'); + end; + + [Test] + procedure GetNextPageTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + MockHttpResponseMessage2: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler"; + MockHttpContent: Codeunit "Http Content"; + MockHttpContent2: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + Success: Boolean; + begin + // [GIVEN] First page with next link + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetMultiPageResponsePage1()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpClientHandler.SetResponse(MockHttpResponseMessage); + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [GIVEN] Get first page + Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); + LibraryAssert.AreEqual(true, Success, 'First page should succeed'); + + // [GIVEN] Mock second page response + MockHttpResponseMessage2.SetHttpStatusCode(200); + MockHttpContent2 := HttpContent.Create(GetMultiPageResponsePage2()); + MockHttpResponseMessage2.SetContent(MockHttpContent2); + MockHttpClientHandler.SetResponse(MockHttpResponseMessage2); + + // [WHEN] GetNextPage is called + Success := GraphClient.GetNextPage(GraphPaginationData, HttpResponseMessage); + + // [THEN] Should be successful + LibraryAssert.AreEqual(true, Success, 'GetNextPage should succeed'); + LibraryAssert.AreEqual(200, HttpResponseMessage.GetHttpStatusCode(), 'Should return 200 status'); + + // [THEN] Should have no more pages (last page) + LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages after last page'); + end; + + [Test] + procedure GetAllPagesTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler"; + MockHttpContent: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + AllResults: JsonArray; + Success: Boolean; + begin + // [GIVEN] Mock multi-page response + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetMultiPageResponsePage1()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpClientHandler.SetResponse(MockHttpResponseMessage); + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // Note: This test is simplified as we can't easily mock multiple sequential responses + // In real scenario, would need enhanced mock to handle multiple calls + + // [WHEN] GetAllPages is called + Success := GraphClient.GetAllPages('users', GraphOptionalParameters, HttpResponseMessage, AllResults); + + // [THEN] Should be successful + LibraryAssert.AreEqual(true, Success, 'GetAllPages should succeed'); + LibraryAssert.AreNotEqual(0, AllResults.Count(), 'Should have results'); + end; + + [Test] + procedure GetWithPaginationPageSizeTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpRequestMessage: Codeunit "Http Request Message"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler"; + MockHttpContent: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + Uri: Codeunit Uri; + begin + // [GIVEN] Mock response + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetSinglePageResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpClientHandler.SetResponse(MockHttpResponseMessage); + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [GIVEN] Set page size + GraphPaginationData.SetPageSize(25); + + // [WHEN] GetWithPagination is called + GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); + + // [THEN] Request should include $top parameter + MockHttpClientHandler.GetHttpRequestMessage(HttpRequestMessage); + Uri.Init(HttpRequestMessage.GetRequestUri()); + LibraryAssert.AreEqual('?$top=25', Uri.GetQuery(), 'Should include page size as $top parameter'); + end; + local procedure GetGroupsResponse(): Text var StringBuilder: TextBuilder; @@ -179,4 +354,61 @@ codeunit 135140 "Graph Client Test" exit(StringBuilder.ToText()); end; + local procedure GetSinglePageResponse(): Text + var + StringBuilder: TextBuilder; + begin + StringBuilder.Append('{'); + StringBuilder.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",'); + StringBuilder.Append(' "value": ['); + StringBuilder.Append(' {'); + StringBuilder.Append(' "id": "87d349ed-44d7-43e1-9a83-5f2406dee5bd",'); + StringBuilder.Append(' "displayName": "Test User 1",'); + StringBuilder.Append(' "mail": "testuser1@contoso.com"'); + StringBuilder.Append(' },'); + StringBuilder.Append(' {'); + StringBuilder.Append(' "id": "45d349ed-44d7-43e1-9a83-5f2406dee5bd",'); + StringBuilder.Append(' "displayName": "Test User 2",'); + StringBuilder.Append(' "mail": "testuser2@contoso.com"'); + StringBuilder.Append(' }'); + StringBuilder.Append(' ]'); + StringBuilder.Append('}'); + exit(StringBuilder.ToText()); + end; + + local procedure GetMultiPageResponsePage1(): Text + var + StringBuilder: TextBuilder; + begin + StringBuilder.Append('{'); + StringBuilder.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",'); + StringBuilder.Append(' "@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$skiptoken=X%274453707402000100000017",'); + StringBuilder.Append(' "value": ['); + StringBuilder.Append(' {'); + StringBuilder.Append(' "id": "87d349ed-44d7-43e1-9a83-5f2406dee5bd",'); + StringBuilder.Append(' "displayName": "Test User 1",'); + StringBuilder.Append(' "mail": "testuser1@contoso.com"'); + StringBuilder.Append(' }'); + StringBuilder.Append(' ]'); + StringBuilder.Append('}'); + exit(StringBuilder.ToText()); + end; + + local procedure GetMultiPageResponsePage2(): Text + var + StringBuilder: TextBuilder; + begin + StringBuilder.Append('{'); + StringBuilder.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",'); + StringBuilder.Append(' "value": ['); + StringBuilder.Append(' {'); + StringBuilder.Append(' "id": "45d349ed-44d7-43e1-9a83-5f2406dee5bd",'); + StringBuilder.Append(' "displayName": "Test User 2",'); + StringBuilder.Append(' "mail": "testuser2@contoso.com"'); + StringBuilder.Append(' }'); + StringBuilder.Append(' ]'); + StringBuilder.Append('}'); + exit(StringBuilder.ToText()); + end; + } \ No newline at end of file diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al new file mode 100644 index 0000000000..d8cae01ce4 --- /dev/null +++ b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Test.Integration.Graph; + +using System.Integration.Graph; +using System.TestLibraries.Utilities; + +codeunit 135143 "Graph Pagination Data Test" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Subtype = Test; + TestPermissions = Disabled; + + var + LibraryAssert: Codeunit "Library Assert"; + + [Test] + procedure InitialStateTest() + var + GraphPaginationData: Codeunit "Graph Pagination Data"; + begin + // [WHEN] GraphPaginationData is initialized + // [THEN] Should have no next link and default page size + LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Initial next link should be empty'); + LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages initially'); + LibraryAssert.AreEqual(100, GraphPaginationData.GetPageSize(), 'Default page size should be 100'); + end; + + [Test] + procedure SetNextLinkTest() + var + GraphPaginationData: Codeunit "Graph Pagination Data"; + NextLink: Text; + begin + // [GIVEN] A next link URL + NextLink := 'https://graph.microsoft.com/v1.0/users?$skiptoken=X%274453707402000100000017'; + + // [WHEN] SetNextLink is called + GraphPaginationData.SetNextLink(NextLink); + + // [THEN] Should store and return the next link + LibraryAssert.AreEqual(NextLink, GraphPaginationData.GetNextLink(), 'Should return the set next link'); + LibraryAssert.AreEqual(true, GraphPaginationData.HasMorePages(), 'Should have more pages when next link is set'); + end; + + [Test] + procedure ClearNextLinkTest() + var + GraphPaginationData: Codeunit "Graph Pagination Data"; + begin + // [GIVEN] A next link is set + GraphPaginationData.SetNextLink('https://graph.microsoft.com/v1.0/users?$skiptoken=123'); + + // [WHEN] Empty next link is set + GraphPaginationData.SetNextLink(''); + + // [THEN] Should have no more pages + LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Next link should be empty'); + LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages'); + end; + + [Test] + procedure SetPageSizeValidTest() + var + GraphPaginationData: Codeunit "Graph Pagination Data"; + begin + // [WHEN] Valid page sizes are set + GraphPaginationData.SetPageSize(1); + LibraryAssert.AreEqual(1, GraphPaginationData.GetPageSize(), 'Should accept minimum page size of 1'); + + GraphPaginationData.SetPageSize(50); + LibraryAssert.AreEqual(50, GraphPaginationData.GetPageSize(), 'Should accept page size of 50'); + + GraphPaginationData.SetPageSize(999); + LibraryAssert.AreEqual(999, GraphPaginationData.GetPageSize(), 'Should accept maximum page size of 999'); + end; + + [Test] + procedure SetPageSizeInvalidTest() + var + GraphPaginationData: Codeunit "Graph Pagination Data"; + begin + // [WHEN] Invalid page size is set (0) + asserterror GraphPaginationData.SetPageSize(0); + LibraryAssert.ExpectedError('Page size must be between 1 and 999.'); + + // [WHEN] Invalid page size is set (negative) + asserterror GraphPaginationData.SetPageSize(-1); + LibraryAssert.ExpectedError('Page size must be between 1 and 999.'); + + // [WHEN] Invalid page size is set (too large) + asserterror GraphPaginationData.SetPageSize(1000); + LibraryAssert.ExpectedError('Page size must be between 1 and 999.'); + end; + + [Test] + procedure GetDefaultPageSizeTest() + var + GraphPaginationData: Codeunit "Graph Pagination Data"; + begin + // [WHEN] GetDefaultPageSize is called + // [THEN] Should return 100 + LibraryAssert.AreEqual(100, GraphPaginationData.GetDefaultPageSize(), 'Default page size should be 100'); + end; + + [Test] + procedure ResetTest() + var + GraphPaginationData: Codeunit "Graph Pagination Data"; + begin + // [GIVEN] GraphPaginationData with values set + GraphPaginationData.SetNextLink('https://graph.microsoft.com/v1.0/users?$skiptoken=123'); + GraphPaginationData.SetPageSize(50); + + // [WHEN] Reset is called + GraphPaginationData.Reset(); + + // [THEN] Should reset to initial state + LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Next link should be empty after reset'); + LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages after reset'); + LibraryAssert.AreEqual(100, GraphPaginationData.GetPageSize(), 'Should return default page size after reset'); + end; + + [Test] + procedure PageSizeZeroReturnsDefaultTest() + var + GraphPaginationData: Codeunit "Graph Pagination Data"; + begin + // [GIVEN] Page size is set to a valid value + GraphPaginationData.SetPageSize(50); + LibraryAssert.AreEqual(50, GraphPaginationData.GetPageSize(), 'Should return set page size'); + + // [WHEN] Reset is called (which clears page size to 0) + GraphPaginationData.Reset(); + + // [THEN] GetPageSize should return default value + LibraryAssert.AreEqual(100, GraphPaginationData.GetPageSize(), 'Should return default page size when internal value is 0'); + end; +} \ No newline at end of file diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationHelperTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationHelperTest.Codeunit.al new file mode 100644 index 0000000000..29bb69fcc8 --- /dev/null +++ b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationHelperTest.Codeunit.al @@ -0,0 +1,218 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Test.Integration.Graph; + +using System.Integration.Graph; +using System.RestClient; +using System.TestLibraries.Utilities; + +codeunit 135144 "Graph Pagination Helper Test" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Subtype = Test; + TestPermissions = Disabled; + + var + LibraryAssert: Codeunit "Library Assert"; + + [Test] + procedure ExtractNextLinkSuccessTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + ResponseText: Text; + begin + // [GIVEN] Successful response with next link + HttpResponseMessage.SetHttpStatusCode(200); + ResponseText := '{"@odata.nextLink":"https://graph.microsoft.com/v1.0/users?$skiptoken=123","value":[]}'; + HttpContent := HttpContent.Create(ResponseText); + HttpResponseMessage.SetContent(HttpContent); + + // [WHEN] ExtractNextLink is called + GraphPaginationHelper.ExtractNextLink(HttpResponseMessage, GraphPaginationData); + + // [THEN] Should extract and set the next link + LibraryAssert.AreEqual('https://graph.microsoft.com/v1.0/users?$skiptoken=123', GraphPaginationData.GetNextLink(), 'Should extract next link'); + LibraryAssert.AreEqual(true, GraphPaginationData.HasMorePages(), 'Should have more pages'); + end; + + [Test] + procedure ExtractNextLinkNoNextLinkTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + ResponseText: Text; + begin + // [GIVEN] Successful response without next link + HttpResponseMessage.SetHttpStatusCode(200); + ResponseText := '{"value":[{"id":"123","displayName":"Test User"}]}'; + HttpContent := HttpContent.Create(ResponseText); + HttpResponseMessage.SetContent(HttpContent); + + // [WHEN] ExtractNextLink is called + GraphPaginationHelper.ExtractNextLink(HttpResponseMessage, GraphPaginationData); + + // [THEN] Should have empty next link + LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Should have empty next link'); + LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages'); + end; + + [Test] + procedure ExtractNextLinkErrorResponseTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + begin + // [GIVEN] Error response + HttpResponseMessage.SetHttpStatusCode(400); + + // [WHEN] ExtractNextLink is called + GraphPaginationHelper.ExtractNextLink(HttpResponseMessage, GraphPaginationData); + + // [THEN] Should have empty next link + LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Should have empty next link on error'); + LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages on error'); + end; + + [Test] + procedure ExtractValueArraySuccessTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + ValueArray: JsonArray; + ResponseText: Text; + Success: Boolean; + begin + // [GIVEN] Successful response with value array + HttpResponseMessage.SetHttpStatusCode(200); + ResponseText := '{"value":[{"id":"1","name":"User1"},{"id":"2","name":"User2"}]}'; + HttpContent := HttpContent.Create(ResponseText); + HttpResponseMessage.SetContent(HttpContent); + + // [WHEN] ExtractValueArray is called + Success := GraphPaginationHelper.ExtractValueArray(HttpResponseMessage, ValueArray); + + // [THEN] Should extract value array successfully + LibraryAssert.AreEqual(true, Success, 'Should extract value array successfully'); + LibraryAssert.AreEqual(2, ValueArray.Count(), 'Should have 2 items in value array'); + end; + + [Test] + procedure ExtractValueArrayNoValueTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + ValueArray: JsonArray; + ResponseText: Text; + Success: Boolean; + begin + // [GIVEN] Response without value array + HttpResponseMessage.SetHttpStatusCode(200); + ResponseText := '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#users"}'; + HttpContent := HttpContent.Create(ResponseText); + HttpResponseMessage.SetContent(HttpContent); + + // [WHEN] ExtractValueArray is called + Success := GraphPaginationHelper.ExtractValueArray(HttpResponseMessage, ValueArray); + + // [THEN] Should fail to extract + LibraryAssert.AreEqual(false, Success, 'Should fail when no value array'); + LibraryAssert.AreEqual(0, ValueArray.Count(), 'Should have empty array'); + end; + + [Test] + procedure ApplyPageSizeTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + ODataParams: Dictionary of [Text, Text]; + begin + // [GIVEN] Page size is set + GraphPaginationData.SetPageSize(25); + + // [WHEN] ApplyPageSize is called + GraphPaginationHelper.ApplyPageSize(GraphOptionalParameters, GraphPaginationData); + + // [THEN] Should set $top parameter + ODataParams := GraphOptionalParameters.GetODataQueryParameters(); + LibraryAssert.AreEqual(true, ODataParams.ContainsKey('$top'), 'Should contain $top parameter'); + LibraryAssert.AreEqual('25', ODataParams.Get('$top'), 'Should set correct page size'); + end; + + [Test] + procedure CombineValueArraysTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + JsonResults: JsonArray; + JsonToken: JsonToken; + Success: Boolean; + begin + // [GIVEN] Initial results array with one item + JsonToken.ReadFrom('{"id":"0","name":"Initial"}'); + JsonResults.Add(JsonToken); + + // [GIVEN] Response with new items + HttpResponseMessage.SetHttpStatusCode(200); + HttpContent := HttpContent.Create('{"value":[{"id":"1","name":"User1"},{"id":"2","name":"User2"}]}'); + HttpResponseMessage.SetContent(HttpContent); + + // [WHEN] CombineValueArrays is called + Success := GraphPaginationHelper.CombineValueArrays(HttpResponseMessage, JsonResults); + + // [THEN] Should combine arrays successfully + LibraryAssert.AreEqual(true, Success, 'Should combine arrays successfully'); + LibraryAssert.AreEqual(3, JsonResults.Count(), 'Should have 3 total items'); + end; + + [Test] + procedure IsWithinIterationLimitTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + IterationCount: Integer; + WithinLimit: Boolean; + begin + // [GIVEN] Iteration count is 0 + IterationCount := 0; + + // [WHEN] IsWithinIterationLimit is called + WithinLimit := GraphPaginationHelper.IsWithinIterationLimit(IterationCount, 5); + + // [THEN] Should be within limit and increment count + LibraryAssert.AreEqual(true, WithinLimit, 'Should be within limit'); + LibraryAssert.AreEqual(1, IterationCount, 'Should increment iteration count'); + + // [GIVEN] Iteration count at limit + IterationCount := 5; + + // [WHEN] IsWithinIterationLimit is called + WithinLimit := GraphPaginationHelper.IsWithinIterationLimit(IterationCount, 5); + + // [THEN] Should not be within limit + LibraryAssert.AreEqual(false, WithinLimit, 'Should not be within limit'); + LibraryAssert.AreEqual(5, IterationCount, 'Should not increment when at limit'); + end; + + [Test] + procedure GetMaxIterationsTest() + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + begin + // [WHEN] GetMaxIterations is called + // [THEN] Should return 1000 + LibraryAssert.AreEqual(1000, GraphPaginationHelper.GetMaxIterations(), 'Max iterations should be 1000'); + end; +} \ No newline at end of file diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al new file mode 100644 index 0000000000..11a891f2a6 --- /dev/null +++ b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al @@ -0,0 +1,314 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Test.Integration.Graph; + +using System.Integration.Graph; +using System.RestClient; +using System.Utilities; +using System.TestLibraries.Utilities; + +codeunit 135146 "Graph Pagination Integ. Test" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Subtype = Test; + TestPermissions = Disabled; + + var + LibraryAssert: Codeunit "Library Assert"; + + [Test] + procedure FullPaginationFlowTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler Multi"; + MockHttpResponseMessage1: Codeunit "Http Response Message"; + MockHttpResponseMessage2: Codeunit "Http Response Message"; + MockHttpResponseMessage3: Codeunit "Http Response Message"; + MockHttpContent1: Codeunit "Http Content"; + MockHttpContent2: Codeunit "Http Content"; + MockHttpContent3: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + AllResults: JsonArray; + Success: Boolean; + begin + // [GIVEN] Three pages of responses + MockHttpResponseMessage1.SetHttpStatusCode(200); + MockHttpContent1 := HttpContent.Create(GetPaginationResponsePage1()); + MockHttpResponseMessage1.SetContent(MockHttpContent1); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage1); + + MockHttpResponseMessage2.SetHttpStatusCode(200); + MockHttpContent2 := HttpContent.Create(GetPaginationResponsePage2()); + MockHttpResponseMessage2.SetContent(MockHttpContent2); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage2); + + MockHttpResponseMessage3.SetHttpStatusCode(200); + MockHttpContent3 := HttpContent.Create(GetPaginationResponsePage3()); + MockHttpResponseMessage3.SetContent(MockHttpContent3); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage3); + + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [WHEN] GetAllPages is called + Success := GraphClient.GetAllPages('users', GraphOptionalParameters, HttpResponseMessage, AllResults); + + // [THEN] Should retrieve all pages successfully + LibraryAssert.AreEqual(true, Success, 'GetAllPages should succeed'); + LibraryAssert.AreEqual(6, AllResults.Count(), 'Should have 6 total users (2 per page)'); + LibraryAssert.AreEqual(3, MockHttpClientHandler.GetRequestCount(), 'Should make 3 requests'); + end; + + [Test] + procedure ManualPaginationFlowTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler Multi"; + MockHttpResponseMessage1: Codeunit "Http Response Message"; + MockHttpResponseMessage2: Codeunit "Http Response Message"; + MockHttpResponseMessage3: Codeunit "Http Response Message"; + MockHttpContent1: Codeunit "Http Content"; + MockHttpContent2: Codeunit "Http Content"; + MockHttpContent3: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + PageCount: Integer; + TotalItems: Integer; + Success: Boolean; + begin + // [GIVEN] Three pages of responses + MockHttpResponseMessage1.SetHttpStatusCode(200); + MockHttpContent1 := HttpContent.Create(GetPaginationResponsePage1()); + MockHttpResponseMessage1.SetContent(MockHttpContent1); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage1); + + MockHttpResponseMessage2.SetHttpStatusCode(200); + MockHttpContent2 := HttpContent.Create(GetPaginationResponsePage2()); + MockHttpResponseMessage2.SetContent(MockHttpContent2); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage2); + + MockHttpResponseMessage3.SetHttpStatusCode(200); + MockHttpContent3 := HttpContent.Create(GetPaginationResponsePage3()); + MockHttpResponseMessage3.SetContent(MockHttpContent3); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage3); + + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [GIVEN] Set page size + GraphPaginationData.SetPageSize(2); + + // [WHEN] Process pages manually + Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); + LibraryAssert.AreEqual(true, Success, 'First page should succeed'); + PageCount := 1; + TotalItems += CountItemsInResponse(HttpResponseMessage); + + while GraphPaginationData.HasMorePages() do begin + Success := GraphClient.GetNextPage(GraphPaginationData, HttpResponseMessage); + LibraryAssert.IsTrue(Success, 'Page should succeed'); + PageCount += 1; + TotalItems += CountItemsInResponse(HttpResponseMessage); + end; + + // [THEN] Should process all pages + LibraryAssert.AreEqual(3, PageCount, 'Should process 3 pages'); + LibraryAssert.AreEqual(6, TotalItems, 'Should have 6 total items'); + LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should have no more pages'); + end; + + [Test] + procedure PaginationWithFiltersTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler Multi"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + MockHttpContent: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + Uri: Codeunit Uri; + QueryString: Text; + begin + // [GIVEN] Response with pagination + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetPaginationResponsePage1()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage); + + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [GIVEN] Set filters and page size + GraphOptionalParameters.SetODataQueryParameter(Enum::"Graph OData Query Parameter"::filter, 'displayName eq ''Test'''); + GraphOptionalParameters.SetODataQueryParameter(Enum::"Graph OData Query Parameter"::select, 'id,displayName'); + GraphPaginationData.SetPageSize(10); + + // [WHEN] GetWithPagination is called + GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); + + // [THEN] Request should include all parameters + QueryString := MockHttpClientHandler.GetHttpRequestUri(1); + Uri.Init(QueryString); + QueryString := Uri.GetQuery(); + + LibraryAssert.AreNotEqual(0, StrPos(QueryString, '$top=10'), 'Should include page size'); + LibraryAssert.AreNotEqual(0, StrPos(QueryString, '$filter=displayName'), 'Should include filter'); + LibraryAssert.AreNotEqual(0, StrPos(QueryString, '$select=id'), 'Should include select'); + end; + + [Test] + procedure PaginationErrorHandlingTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler Multi"; + MockHttpResponseMessage1: Codeunit "Http Response Message"; + MockHttpResponseMessage2: Codeunit "Http Response Message"; + MockHttpContent1: Codeunit "Http Content"; + MockHttpContent2: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + Success: Boolean; + begin + // [GIVEN] First page succeeds, second page fails + MockHttpResponseMessage1.SetHttpStatusCode(200); + MockHttpContent1 := HttpContent.Create(GetPaginationResponsePage1()); + MockHttpResponseMessage1.SetContent(MockHttpContent1); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage1); + + MockHttpResponseMessage2.SetHttpStatusCode(429); // Too Many Requests + MockHttpContent2 := HttpContent.Create('{"error":{"code":"TooManyRequests","message":"Rate limit exceeded"}}'); + MockHttpResponseMessage2.SetContent(MockHttpContent2); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage2); + + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [WHEN] Process pages + Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); + LibraryAssert.AreEqual(true, Success, 'First page should succeed'); + LibraryAssert.AreEqual(true, GraphPaginationData.HasMorePages(), 'Should have more pages'); + + // [WHEN] Second page fails + Success := GraphClient.GetNextPage(GraphPaginationData, HttpResponseMessage); + + // [THEN] Should handle error gracefully + LibraryAssert.AreEqual(false, Success, 'Second page should fail'); + LibraryAssert.AreEqual(429, HttpResponseMessage.GetHttpStatusCode(), 'Should return 429 status'); + end; + + [Test] + procedure GetAllPagesWithMaxIterationTest() + var + GraphAuthSpy: Codeunit "Graph Auth. Spy"; + GraphClient: Codeunit "Graph Client"; + GraphOptionalParameters: Codeunit "Graph Optional Parameters"; + HttpResponseMessage: Codeunit "Http Response Message"; + MockHttpClientHandler: Codeunit "Mock Http Client Handler Multi"; + MockHttpResponseMessage: Codeunit "Http Response Message"; + MockHttpContent: Codeunit "Http Content"; + HttpContent: Codeunit "Http Content"; + AllResults: JsonArray; + i: Integer; + Success: Boolean; + begin + // [GIVEN] Many pages (simulate endless pagination) + for i := 1 to 1005 do begin + MockHttpResponseMessage.SetHttpStatusCode(200); + MockHttpContent := HttpContent.Create(GetEndlessPaginationResponse()); + MockHttpResponseMessage.SetContent(MockHttpContent); + MockHttpClientHandler.AddResponse(MockHttpResponseMessage); + end; + + GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthSpy, MockHttpClientHandler); + + // [WHEN] GetAllPages is called + Success := GraphClient.GetAllPages('users', GraphOptionalParameters, HttpResponseMessage, AllResults); + + // [THEN] Should stop at max iterations (1000) + LibraryAssert.AreEqual(true, Success, 'Should succeed even with max iterations'); + LibraryAssert.AreEqual(1001, MockHttpClientHandler.GetRequestCount(), 'Should make 1001 requests (1 initial + 1000 iterations)'); + end; + + local procedure GetPaginationResponsePage1(): Text + var + StringBuilder: TextBuilder; + begin + StringBuilder.Append('{'); + StringBuilder.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",'); + StringBuilder.Append(' "@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$skiptoken=page2",'); + StringBuilder.Append(' "value": ['); + StringBuilder.Append(' {"id": "1", "displayName": "User 1"},'); + StringBuilder.Append(' {"id": "2", "displayName": "User 2"}'); + StringBuilder.Append(' ]'); + StringBuilder.Append('}'); + exit(StringBuilder.ToText()); + end; + + local procedure GetPaginationResponsePage2(): Text + var + StringBuilder: TextBuilder; + begin + StringBuilder.Append('{'); + StringBuilder.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",'); + StringBuilder.Append(' "@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$skiptoken=page3",'); + StringBuilder.Append(' "value": ['); + StringBuilder.Append(' {"id": "3", "displayName": "User 3"},'); + StringBuilder.Append(' {"id": "4", "displayName": "User 4"}'); + StringBuilder.Append(' ]'); + StringBuilder.Append('}'); + exit(StringBuilder.ToText()); + end; + + local procedure GetPaginationResponsePage3(): Text + var + StringBuilder: TextBuilder; + begin + StringBuilder.Append('{'); + StringBuilder.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",'); + StringBuilder.Append(' "value": ['); + StringBuilder.Append(' {"id": "5", "displayName": "User 5"},'); + StringBuilder.Append(' {"id": "6", "displayName": "User 6"}'); + StringBuilder.Append(' ]'); + StringBuilder.Append('}'); + exit(StringBuilder.ToText()); + end; + + local procedure GetEndlessPaginationResponse(): Text + var + StringBuilder: TextBuilder; + begin + StringBuilder.Append('{'); + StringBuilder.Append(' "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users",'); + StringBuilder.Append(' "@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$skiptoken=endless",'); + StringBuilder.Append(' "value": [{"id": "x", "displayName": "User X"}]'); + StringBuilder.Append('}'); + exit(StringBuilder.ToText()); + end; + + local procedure CountItemsInResponse(HttpResponseMessage: Codeunit "Http Response Message"): Integer + var + ResponseJson: JsonObject; + ValueArray: JsonArray; + JsonToken: JsonToken; + ResponseText: Text; + begin + ResponseText := HttpResponseMessage.GetContent().AsText(); + if ResponseJson.ReadFrom(ResponseText) then + if ResponseJson.Get('value', JsonToken) then begin + ValueArray := JsonToken.AsArray(); + exit(ValueArray.Count()); + end; + end; +} \ No newline at end of file diff --git a/src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al new file mode 100644 index 0000000000..216a61ecbd --- /dev/null +++ b/src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al @@ -0,0 +1,85 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.Test.Integration.Graph; + +using System.RestClient; + +codeunit 135145 "Mock Http Client Handler Multi" implements "Http Client Handler" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + _httpRequestMessages: List of [Text]; + _httpResponseBodies: List of [Text]; + _httpResponseStatusCodes: List of [Integer]; + _currentResponseIndex: Integer; + _sendError: Text; + + procedure Send(HttpClient: HttpClient; HttpRequestMessage: Codeunit System.RestClient."Http Request Message"; var HttpResponseMessage: Codeunit System.RestClient."Http Response Message") Success: Boolean; + begin + ClearLastError(); + exit(TrySend(HttpRequestMessage, HttpResponseMessage)); + end; + + procedure ExpectSendToFailWithError(SendError: Text) + begin + _sendError := SendError; + end; + + procedure AddResponse(StatusCode: Integer; ResponseBody: Text) + begin + _httpResponseStatusCodes.Add(StatusCode); + _httpResponseBodies.Add(ResponseBody); + end; + + procedure AddResponse(var NewHttpResponseMessage: Codeunit System.RestClient."Http Response Message") + var + ResponseBody: Text; + begin + ResponseBody := NewHttpResponseMessage.GetContent().AsText(); + AddResponse(NewHttpResponseMessage.GetHttpStatusCode(), ResponseBody); + end; + + procedure GetHttpRequestUri(Index: Integer): Text + begin + if (Index > 0) and (Index <= _httpRequestMessages.Count()) then + exit(_httpRequestMessages.Get(Index)); + end; + + procedure GetRequestCount(): Integer + begin + exit(_httpRequestMessages.Count()); + end; + + procedure Reset() + begin + Clear(_httpRequestMessages); + Clear(_httpResponseBodies); + Clear(_httpResponseStatusCodes); + _currentResponseIndex := 0; + _sendError := ''; + end; + + [TryFunction] + local procedure TrySend(HttpRequestMessage: Codeunit System.RestClient."Http Request Message"; var HttpResponseMessage: Codeunit System.RestClient."Http Response Message") + var + HttpContent: Codeunit "Http Content"; + begin + _httpRequestMessages.Add(HttpRequestMessage.GetRequestUri()); + + if _sendError <> '' then + Error(_sendError); + + _currentResponseIndex += 1; + if (_currentResponseIndex > 0) and (_currentResponseIndex <= _httpResponseBodies.Count()) then begin + HttpResponseMessage.SetHttpStatusCode(_httpResponseStatusCodes.Get(_currentResponseIndex)); + HttpContent := HttpContent.Create(_httpResponseBodies.Get(_currentResponseIndex)); + HttpResponseMessage.SetContent(HttpContent); + end else + Error('No more mock responses available. Request index: %1, Available responses: %2', _currentResponseIndex, _httpResponseBodies.Count()); + end; +} \ No newline at end of file From fa8de3c94449d13d930bda077bcb6a26d9e89a72 Mon Sep 17 00:00:00 2001 From: Volodymyr Dvernytskyi Date: Fri, 11 Jul 2025 16:30:32 +0300 Subject: [PATCH 3/6] Tests: Replace AreEqual method to IsTrue/IsFale for boolean values --- .../src/GraphClientTest.Codeunit.al | 20 +++++++++---------- .../src/GraphPaginationDataTest.Codeunit.al | 8 ++++---- .../src/GraphPaginationHelperTest.Codeunit.al | 18 ++++++++--------- .../src/GraphPaginationIntegTest.Codeunit.al | 12 +++++------ 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al index 9f63faac49..22db6a52ff 100644 --- a/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al +++ b/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al @@ -38,7 +38,7 @@ codeunit 135140 "Graph Client Test" GraphClient.Get('groups', HttpResponseMessage); // [THEN] Verify authorization of request is triggered - LibraryAssert.AreEqual(true, GraphAuthSpy.IsInvoked(), 'Authorization should be invoked.'); + LibraryAssert.IsTrue(GraphAuthSpy.IsInvoked(), 'Authorization should be invoked.'); end; [Test] @@ -118,7 +118,7 @@ codeunit 135140 "Graph Client Test" GraphClient.Get('groups', HttpResponseMessage); // [THEN] Verify response is correct - LibraryAssert.AreEqual(true, HttpResponseMessage.GetIsSuccessStatusCode(), 'Should be success status code.'); + LibraryAssert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'Should be success status code.'); HttpContent := HttpResponseMessage.GetContent(); ResponseInStream := HttpContent.AsInStream(); ResponseJsonObject.ReadFrom(ResponseInStream); @@ -154,11 +154,11 @@ codeunit 135140 "Graph Client Test" Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); // [THEN] Should be successful - LibraryAssert.AreEqual(true, Success, 'GetWithPagination should succeed'); + LibraryAssert.IsTrue(Success, 'GetWithPagination should succeed'); LibraryAssert.AreEqual(200, HttpResponseMessage.GetHttpStatusCode(), 'Should return 200 status'); // [THEN] Should have no more pages - LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages'); + LibraryAssert.IsFalse(GraphPaginationData.HasMorePages(), 'Should not have more pages'); end; [Test] @@ -186,8 +186,8 @@ codeunit 135140 "Graph Client Test" Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); // [THEN] Should be successful and have more pages - LibraryAssert.AreEqual(true, Success, 'GetWithPagination should succeed'); - LibraryAssert.AreEqual(true, GraphPaginationData.HasMorePages(), 'Should have more pages'); + LibraryAssert.IsTrue(Success, 'GetWithPagination should succeed'); + LibraryAssert.IsTrue(GraphPaginationData.HasMorePages(), 'Should have more pages'); LibraryAssert.AreNotEqual('', GraphPaginationData.GetNextLink(), 'Should have next link'); end; @@ -216,7 +216,7 @@ codeunit 135140 "Graph Client Test" // [GIVEN] Get first page Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); - LibraryAssert.AreEqual(true, Success, 'First page should succeed'); + LibraryAssert.IsTrue(Success, 'First page should succeed'); // [GIVEN] Mock second page response MockHttpResponseMessage2.SetHttpStatusCode(200); @@ -228,11 +228,11 @@ codeunit 135140 "Graph Client Test" Success := GraphClient.GetNextPage(GraphPaginationData, HttpResponseMessage); // [THEN] Should be successful - LibraryAssert.AreEqual(true, Success, 'GetNextPage should succeed'); + LibraryAssert.IsTrue(Success, 'GetNextPage should succeed'); LibraryAssert.AreEqual(200, HttpResponseMessage.GetHttpStatusCode(), 'Should return 200 status'); // [THEN] Should have no more pages (last page) - LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages after last page'); + LibraryAssert.IsFalse(GraphPaginationData.HasMorePages(), 'Should not have more pages after last page'); end; [Test] @@ -263,7 +263,7 @@ codeunit 135140 "Graph Client Test" Success := GraphClient.GetAllPages('users', GraphOptionalParameters, HttpResponseMessage, AllResults); // [THEN] Should be successful - LibraryAssert.AreEqual(true, Success, 'GetAllPages should succeed'); + LibraryAssert.IsTrue(Success, 'GetAllPages should succeed'); LibraryAssert.AreNotEqual(0, AllResults.Count(), 'Should have results'); end; diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al index d8cae01ce4..bc22224037 100644 --- a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al +++ b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al @@ -26,7 +26,7 @@ codeunit 135143 "Graph Pagination Data Test" // [WHEN] GraphPaginationData is initialized // [THEN] Should have no next link and default page size LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Initial next link should be empty'); - LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages initially'); + LibraryAssert.IsFalse(GraphPaginationData.HasMorePages(), 'Should not have more pages initially'); LibraryAssert.AreEqual(100, GraphPaginationData.GetPageSize(), 'Default page size should be 100'); end; @@ -44,7 +44,7 @@ codeunit 135143 "Graph Pagination Data Test" // [THEN] Should store and return the next link LibraryAssert.AreEqual(NextLink, GraphPaginationData.GetNextLink(), 'Should return the set next link'); - LibraryAssert.AreEqual(true, GraphPaginationData.HasMorePages(), 'Should have more pages when next link is set'); + LibraryAssert.IsTrue(GraphPaginationData.HasMorePages(), 'Should have more pages when next link is set'); end; [Test] @@ -60,7 +60,7 @@ codeunit 135143 "Graph Pagination Data Test" // [THEN] Should have no more pages LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Next link should be empty'); - LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages'); + LibraryAssert.IsFalse(GraphPaginationData.HasMorePages(), 'Should not have more pages'); end; [Test] @@ -121,7 +121,7 @@ codeunit 135143 "Graph Pagination Data Test" // [THEN] Should reset to initial state LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Next link should be empty after reset'); - LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages after reset'); + LibraryAssert.IsFalse(GraphPaginationData.HasMorePages(), 'Should not have more pages after reset'); LibraryAssert.AreEqual(100, GraphPaginationData.GetPageSize(), 'Should return default page size after reset'); end; diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationHelperTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationHelperTest.Codeunit.al index 29bb69fcc8..9b94a119d7 100644 --- a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationHelperTest.Codeunit.al +++ b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationHelperTest.Codeunit.al @@ -39,7 +39,7 @@ codeunit 135144 "Graph Pagination Helper Test" // [THEN] Should extract and set the next link LibraryAssert.AreEqual('https://graph.microsoft.com/v1.0/users?$skiptoken=123', GraphPaginationData.GetNextLink(), 'Should extract next link'); - LibraryAssert.AreEqual(true, GraphPaginationData.HasMorePages(), 'Should have more pages'); + LibraryAssert.IsTrue(GraphPaginationData.HasMorePages(), 'Should have more pages'); end; [Test] @@ -62,7 +62,7 @@ codeunit 135144 "Graph Pagination Helper Test" // [THEN] Should have empty next link LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Should have empty next link'); - LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages'); + LibraryAssert.IsFalse(GraphPaginationData.HasMorePages(), 'Should not have more pages'); end; [Test] @@ -80,7 +80,7 @@ codeunit 135144 "Graph Pagination Helper Test" // [THEN] Should have empty next link LibraryAssert.AreEqual('', GraphPaginationData.GetNextLink(), 'Should have empty next link on error'); - LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should not have more pages on error'); + LibraryAssert.IsFalse(GraphPaginationData.HasMorePages(), 'Should not have more pages on error'); end; [Test] @@ -103,7 +103,7 @@ codeunit 135144 "Graph Pagination Helper Test" Success := GraphPaginationHelper.ExtractValueArray(HttpResponseMessage, ValueArray); // [THEN] Should extract value array successfully - LibraryAssert.AreEqual(true, Success, 'Should extract value array successfully'); + LibraryAssert.IsTrue(Success, 'Should extract value array successfully'); LibraryAssert.AreEqual(2, ValueArray.Count(), 'Should have 2 items in value array'); end; @@ -127,7 +127,7 @@ codeunit 135144 "Graph Pagination Helper Test" Success := GraphPaginationHelper.ExtractValueArray(HttpResponseMessage, ValueArray); // [THEN] Should fail to extract - LibraryAssert.AreEqual(false, Success, 'Should fail when no value array'); + LibraryAssert.IsFalse(Success, 'Should fail when no value array'); LibraryAssert.AreEqual(0, ValueArray.Count(), 'Should have empty array'); end; @@ -147,7 +147,7 @@ codeunit 135144 "Graph Pagination Helper Test" // [THEN] Should set $top parameter ODataParams := GraphOptionalParameters.GetODataQueryParameters(); - LibraryAssert.AreEqual(true, ODataParams.ContainsKey('$top'), 'Should contain $top parameter'); + LibraryAssert.IsTrue(ODataParams.ContainsKey('$top'), 'Should contain $top parameter'); LibraryAssert.AreEqual('25', ODataParams.Get('$top'), 'Should set correct page size'); end; @@ -174,7 +174,7 @@ codeunit 135144 "Graph Pagination Helper Test" Success := GraphPaginationHelper.CombineValueArrays(HttpResponseMessage, JsonResults); // [THEN] Should combine arrays successfully - LibraryAssert.AreEqual(true, Success, 'Should combine arrays successfully'); + LibraryAssert.IsTrue(Success, 'Should combine arrays successfully'); LibraryAssert.AreEqual(3, JsonResults.Count(), 'Should have 3 total items'); end; @@ -192,7 +192,7 @@ codeunit 135144 "Graph Pagination Helper Test" WithinLimit := GraphPaginationHelper.IsWithinIterationLimit(IterationCount, 5); // [THEN] Should be within limit and increment count - LibraryAssert.AreEqual(true, WithinLimit, 'Should be within limit'); + LibraryAssert.IsTrue(WithinLimit, 'Should be within limit'); LibraryAssert.AreEqual(1, IterationCount, 'Should increment iteration count'); // [GIVEN] Iteration count at limit @@ -202,7 +202,7 @@ codeunit 135144 "Graph Pagination Helper Test" WithinLimit := GraphPaginationHelper.IsWithinIterationLimit(IterationCount, 5); // [THEN] Should not be within limit - LibraryAssert.AreEqual(false, WithinLimit, 'Should not be within limit'); + LibraryAssert.IsFalse(WithinLimit, 'Should not be within limit'); LibraryAssert.AreEqual(5, IterationCount, 'Should not increment when at limit'); end; diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al index 11a891f2a6..9d8b52c008 100644 --- a/src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al +++ b/src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al @@ -60,7 +60,7 @@ codeunit 135146 "Graph Pagination Integ. Test" Success := GraphClient.GetAllPages('users', GraphOptionalParameters, HttpResponseMessage, AllResults); // [THEN] Should retrieve all pages successfully - LibraryAssert.AreEqual(true, Success, 'GetAllPages should succeed'); + LibraryAssert.IsTrue(Success, 'GetAllPages should succeed'); LibraryAssert.AreEqual(6, AllResults.Count(), 'Should have 6 total users (2 per page)'); LibraryAssert.AreEqual(3, MockHttpClientHandler.GetRequestCount(), 'Should make 3 requests'); end; @@ -108,7 +108,7 @@ codeunit 135146 "Graph Pagination Integ. Test" // [WHEN] Process pages manually Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); - LibraryAssert.AreEqual(true, Success, 'First page should succeed'); + LibraryAssert.IsTrue(Success, 'First page should succeed'); PageCount := 1; TotalItems += CountItemsInResponse(HttpResponseMessage); @@ -122,7 +122,7 @@ codeunit 135146 "Graph Pagination Integ. Test" // [THEN] Should process all pages LibraryAssert.AreEqual(3, PageCount, 'Should process 3 pages'); LibraryAssert.AreEqual(6, TotalItems, 'Should have 6 total items'); - LibraryAssert.AreEqual(false, GraphPaginationData.HasMorePages(), 'Should have no more pages'); + LibraryAssert.IsFalse(GraphPaginationData.HasMorePages(), 'Should have no more pages'); end; [Test] @@ -197,8 +197,8 @@ codeunit 135146 "Graph Pagination Integ. Test" // [WHEN] Process pages Success := GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage); - LibraryAssert.AreEqual(true, Success, 'First page should succeed'); - LibraryAssert.AreEqual(true, GraphPaginationData.HasMorePages(), 'Should have more pages'); + LibraryAssert.IsTrue(Success, 'First page should succeed'); + LibraryAssert.IsTrue(GraphPaginationData.HasMorePages(), 'Should have more pages'); // [WHEN] Second page fails Success := GraphClient.GetNextPage(GraphPaginationData, HttpResponseMessage); @@ -237,7 +237,7 @@ codeunit 135146 "Graph Pagination Integ. Test" Success := GraphClient.GetAllPages('users', GraphOptionalParameters, HttpResponseMessage, AllResults); // [THEN] Should stop at max iterations (1000) - LibraryAssert.AreEqual(true, Success, 'Should succeed even with max iterations'); + LibraryAssert.IsTrue(Success, 'Should succeed even with max iterations'); LibraryAssert.AreEqual(1001, MockHttpClientHandler.GetRequestCount(), 'Should make 1001 requests (1 initial + 1000 iterations)'); end; From 2bc6378f84682d26f6fd795cb964437158290c51 Mon Sep 17 00:00:00 2001 From: Volodymyr Dvernytskyi Date: Mon, 18 Aug 2025 15:31:14 +0300 Subject: [PATCH 4/6] Add internals visible to graph test app --- src/System Application/App/MicrosoftGraph/app.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/System Application/App/MicrosoftGraph/app.json b/src/System Application/App/MicrosoftGraph/app.json index 8f08ee299c..7d05c9c59b 100644 --- a/src/System Application/App/MicrosoftGraph/app.json +++ b/src/System Application/App/MicrosoftGraph/app.json @@ -47,6 +47,11 @@ "id": "da17b564-d600-44d5-be0b-ca7ff7ac26fc", "name": "Azure AD Graph Test Library", "publisher": "Microsoft" + }, + { + "id": "2746dab0-7900-449d-b154-20751e116a67", + "name": "Microsoft Graph Test", + "publisher": "Microsoft" } ], "screenshots": [], From a640f2f8da256fbd9cda69689beb607a14002675 Mon Sep 17 00:00:00 2001 From: Jesper Schulz-Wedde Date: Tue, 19 Aug 2025 12:20:28 +0200 Subject: [PATCH 5/6] Fix tiny typo --- .../App/MicrosoftGraph/src/GraphClient.Codeunit.al | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al b/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al index 9f620a0ecb..7c0b023dd6 100644 --- a/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al @@ -146,7 +146,7 @@ codeunit 9350 "Graph Client" /// Get any request to the microsoft graph API with pagination support /// /// Does not require UI interaction. This method handles pagination automatically. - /// A relativ uri including the resource segments + /// A relative uri including the resource segments /// A wrapper for optional header and query parameters /// The pagination data object to track pagination state /// The response message object. @@ -174,7 +174,7 @@ codeunit 9350 "Graph Client" /// Get all pages of results automatically /// /// Does not require UI interaction. This method fetches all pages automatically and returns the combined results. - /// A relativ uri including the resource segments + /// A relative uri including the resource segments /// A wrapper for optional header and query parameters /// The last response message object. /// A JSON array containing all results from all pages From 1ae3c9d0d161bac2a80dad0e25bf25d245da31f8 Mon Sep 17 00:00:00 2001 From: Volodymyr Dvernytskyi Date: Wed, 20 Aug 2025 10:55:30 +0300 Subject: [PATCH 6/6] Replace underscore with this. for global variables --- .../MockHttpClientHandlerMulti.Codeunit.al | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al index 216a61ecbd..8e7be7138c 100644 --- a/src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al +++ b/src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al @@ -13,11 +13,11 @@ codeunit 135145 "Mock Http Client Handler Multi" implements "Http Client Handler InherentPermissions = X; var - _httpRequestMessages: List of [Text]; - _httpResponseBodies: List of [Text]; - _httpResponseStatusCodes: List of [Integer]; - _currentResponseIndex: Integer; - _sendError: Text; + httpRequestMessages: List of [Text]; + httpResponseBodies: List of [Text]; + httpResponseStatusCodes: List of [Integer]; + currentResponseIndex: Integer; + sendError: Text; procedure Send(HttpClient: HttpClient; HttpRequestMessage: Codeunit System.RestClient."Http Request Message"; var HttpResponseMessage: Codeunit System.RestClient."Http Response Message") Success: Boolean; begin @@ -25,15 +25,15 @@ codeunit 135145 "Mock Http Client Handler Multi" implements "Http Client Handler exit(TrySend(HttpRequestMessage, HttpResponseMessage)); end; - procedure ExpectSendToFailWithError(SendError: Text) + procedure ExpectSendToFailWithError(NewSendError: Text) begin - _sendError := SendError; + this.SendError := NewSendError; end; procedure AddResponse(StatusCode: Integer; ResponseBody: Text) begin - _httpResponseStatusCodes.Add(StatusCode); - _httpResponseBodies.Add(ResponseBody); + this.httpResponseStatusCodes.Add(StatusCode); + this.httpResponseBodies.Add(ResponseBody); end; procedure AddResponse(var NewHttpResponseMessage: Codeunit System.RestClient."Http Response Message") @@ -46,22 +46,22 @@ codeunit 135145 "Mock Http Client Handler Multi" implements "Http Client Handler procedure GetHttpRequestUri(Index: Integer): Text begin - if (Index > 0) and (Index <= _httpRequestMessages.Count()) then - exit(_httpRequestMessages.Get(Index)); + if (Index > 0) and (Index <= this.httpRequestMessages.Count()) then + exit(this.httpRequestMessages.Get(Index)); end; procedure GetRequestCount(): Integer begin - exit(_httpRequestMessages.Count()); + exit(this.httpRequestMessages.Count()); end; procedure Reset() begin - Clear(_httpRequestMessages); - Clear(_httpResponseBodies); - Clear(_httpResponseStatusCodes); - _currentResponseIndex := 0; - _sendError := ''; + Clear(this.httpRequestMessages); + Clear(this.httpResponseBodies); + Clear(this.httpResponseStatusCodes); + this.currentResponseIndex := 0; + this.sendError := ''; end; [TryFunction] @@ -69,17 +69,17 @@ codeunit 135145 "Mock Http Client Handler Multi" implements "Http Client Handler var HttpContent: Codeunit "Http Content"; begin - _httpRequestMessages.Add(HttpRequestMessage.GetRequestUri()); + this.httpRequestMessages.Add(HttpRequestMessage.GetRequestUri()); - if _sendError <> '' then - Error(_sendError); + if this.sendError <> '' then + Error(this.sendError); - _currentResponseIndex += 1; - if (_currentResponseIndex > 0) and (_currentResponseIndex <= _httpResponseBodies.Count()) then begin - HttpResponseMessage.SetHttpStatusCode(_httpResponseStatusCodes.Get(_currentResponseIndex)); - HttpContent := HttpContent.Create(_httpResponseBodies.Get(_currentResponseIndex)); + this.currentResponseIndex += 1; + if (this.currentResponseIndex > 0) and (this.currentResponseIndex <= this.httpResponseBodies.Count()) then begin + HttpResponseMessage.SetHttpStatusCode(this.httpResponseStatusCodes.Get(this.currentResponseIndex)); + HttpContent := HttpContent.Create(this.httpResponseBodies.Get(this.currentResponseIndex)); HttpResponseMessage.SetContent(HttpContent); end else - Error('No more mock responses available. Request index: %1, Available responses: %2', _currentResponseIndex, _httpResponseBodies.Count()); + Error('No more mock responses available. Request index: %1, Available responses: %2', this.currentResponseIndex, this.httpResponseBodies.Count()); end; } \ No newline at end of file