diff --git a/src/System Application/App/MicrosoftGraph/app.json b/src/System Application/App/MicrosoftGraph/app.json index bd22a00b21..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": [], @@ -54,7 +59,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..7c0b023dd6 100644 --- a/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al @@ -139,4 +139,51 @@ 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 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. + /// 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 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 + /// True if the operation was successful; otherwise - false. + /// Authentication failed. + 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, HttpResponseMessage, 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..f73b14f770 100644 --- a/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al +++ b/src/System Application/App/MicrosoftGraph/src/GraphClientImpl.Codeunit.al @@ -99,5 +99,63 @@ 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 HttpResponseMessage: Codeunit "Http Response Message"; var JsonResults: JsonArray): Boolean + var + GraphPaginationHelper: Codeunit "Graph Pagination Helper"; + GraphPaginationData: Codeunit "Graph Pagination Data"; + 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 diff --git a/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al b/src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al index 72db540ee9..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); @@ -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.IsTrue(Success, 'GetWithPagination should succeed'); + LibraryAssert.AreEqual(200, HttpResponseMessage.GetHttpStatusCode(), 'Should return 200 status'); + + // [THEN] Should have no more pages + LibraryAssert.IsFalse(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.IsTrue(Success, 'GetWithPagination should succeed'); + LibraryAssert.IsTrue(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.IsTrue(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.IsTrue(Success, 'GetNextPage should succeed'); + LibraryAssert.AreEqual(200, HttpResponseMessage.GetHttpStatusCode(), 'Should return 200 status'); + + // [THEN] Should have no more pages (last page) + LibraryAssert.IsFalse(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.IsTrue(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..bc22224037 --- /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.IsFalse(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.IsTrue(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.IsFalse(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.IsFalse(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..9b94a119d7 --- /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.IsTrue(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.IsFalse(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.IsFalse(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.IsTrue(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.IsFalse(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.IsTrue(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.IsTrue(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.IsTrue(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.IsFalse(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..9d8b52c008 --- /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.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; + + [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.IsTrue(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.IsFalse(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.IsTrue(Success, 'First page should succeed'); + LibraryAssert.IsTrue(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.IsTrue(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..8e7be7138c --- /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(NewSendError: Text) + begin + this.SendError := NewSendError; + end; + + procedure AddResponse(StatusCode: Integer; ResponseBody: Text) + begin + this.httpResponseStatusCodes.Add(StatusCode); + this.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 <= this.httpRequestMessages.Count()) then + exit(this.httpRequestMessages.Get(Index)); + end; + + procedure GetRequestCount(): Integer + begin + exit(this.httpRequestMessages.Count()); + end; + + procedure Reset() + begin + Clear(this.httpRequestMessages); + Clear(this.httpResponseBodies); + Clear(this.httpResponseStatusCodes); + this.currentResponseIndex := 0; + this.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 + this.httpRequestMessages.Add(HttpRequestMessage.GetRequestUri()); + + if this.sendError <> '' then + Error(this.sendError); + + 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', this.currentResponseIndex, this.httpResponseBodies.Count()); + end; +} \ No newline at end of file