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