This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 1ac74beca fix(cloud): consolidate inventory pagination (#2956)
1ac74beca is described below

commit 1ac74becacad1457130c1cd521c1b0545cd24242
Author: aias00 <[email protected]>
AuthorDate: Wed Sep 2 15:03:45 2026 +0800

    fix(cloud): consolidate inventory pagination (#2956)
    
    * fix(tencent): complete cloud inventory pagination
    
    (cherry picked from commit e0bcd31a785e7d115f8fb7513984258a46896004)
    Signed-off-by: liuhy <[email protected]>
    
    * fix(studio): page Tencent ACL inventories by total count
    
    Tencent Cloud exposes TotalCount for ACL roles and topic subscriptions, so 
Studio should not silently stop at the old 100-page inventory ceiling.
    
    Constraint: Preserve short-page termination when Tencent omits TotalCount.
    
    Rejected: Keep MAX_PAGES guard | it still truncates valid inventories 
beyond 10,000 records.
    
    Confidence: high
    
    Scope-risk: narrow
    
    Tested: 
JAVA_HOME=/Users/aias/Library/Java/JavaVirtualMachines/openjdk-21.0.2/Contents/Home
 mvn -Dtest=TencentAclServiceTest,TencentInstanceProviderTest test
    
    Tested: 
JAVA_HOME=/Users/aias/Library/Java/JavaVirtualMachines/openjdk-21.0.2/Contents/Home
 mvn -Dtest='org.apache.rocketmq.studio.provider.tencent.*Test' test
    
    Not-tested: Full mvn test is blocked by pre-existing origin/rocketmq-studio 
AlertSchemaMigrationTest failure: rmq_instance_message table not found.
    Signed-off-by: liuhy <[email protected]>
    (cherry picked from commit 712b6c6ee190f5edbbb90adfd08614e1524520aa)
    Signed-off-by: liuhy <[email protected]>
    
    * fix(studio): fetch all Aliyun consumer groups
    
    Aliyun consumer group inventories must not stop at the legacy five-page cap 
when the OpenAPI response reports more data. Continue until totalCount is 
satisfied or the service returns a short page so counts, exports, and 
management screens see the full group set.\n\nConstraint: refreshed against 
origin/rocketmq-studio at 899561e56.\nRejected: keep MAX_PAGES fallback | it 
still returns a successful partial inventory for known totals above 
500.\nConfidence: high\nScope-risk: narrow\nTest [...]
    
    Signed-off-by: liuhy <[email protected]>
    (cherry picked from commit e59ac28decda2400d3075ba431b6733aa2f13ee0)
    Signed-off-by: liuhy <[email protected]>
    
    ---------
    
    Signed-off-by: liuhy <[email protected]>
---
 .../provider/alibaba/AliyunInstanceProvider.java   |  5 +-
 .../studio/provider/tencent/TencentAclService.java | 53 +++++++-------
 .../provider/tencent/TencentCatalogService.java    | 11 +--
 .../provider/tencent/TencentInstanceProvider.java  | 18 +++--
 .../alibaba/AliyunInstanceProviderTest.java        | 73 ++++++++++++++++++++
 .../provider/tencent/TencentAclServiceTest.java    | 80 ++++++++++++++++++++++
 .../tencent/TencentCatalogServiceTest.java         | 54 +++++++++++++++
 .../tencent/TencentInstanceProviderTest.java       | 80 ++++++++++++++++++++++
 8 files changed, 337 insertions(+), 37 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
index 3cf687d62..08d7e8070 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProvider.java
@@ -301,7 +301,7 @@ public class AliyunInstanceProvider implements 
InstanceProvider {
     public List<ConsumerGroupVO> listConsumerGroups(String instanceId, String 
search) {
         Context ctx = resolve(instanceId);
         List<ListConsumerGroupsResponseBody.List> all = new ArrayList<>();
-        for (int page = 1; page <= AliyunConverters.MAX_PAGES; page++) {
+        for (int page = 1; ; page++) {
             ListConsumerGroupsRequest.Builder builder = 
ListConsumerGroupsRequest.builder()
                     .instanceId(ctx.cloudInstanceId())
                     .pageNumber(page)
@@ -319,7 +319,8 @@ public class AliyunInstanceProvider implements 
InstanceProvider {
                 break;
             }
             all.addAll(list);
-            if (list.size() < AliyunConverters.PAGE_SIZE) {
+            if (hasFetchedAll(page, AliyunConverters.PAGE_SIZE, 
data.getTotalCount())
+                    || list.size() < AliyunConverters.PAGE_SIZE) {
                 break;
             }
         }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentAclService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentAclService.java
index 03b858e28..b656056e6 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentAclService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentAclService.java
@@ -53,7 +53,6 @@ import java.util.List;
 public class TencentAclService {
 
     static final int PAGE_SIZE = 100;
-    static final int MAX_PAGES = 100;
     static final String ACL_VERSION = "1.0";
     static final String RESOURCE_TYPE = "Cluster";
     static final String RESOURCE = "*";
@@ -67,23 +66,20 @@ public class TencentAclService {
     public List<AclUserVO> listUsers(String instanceId) {
         Context context = resolve(instanceId);
         List<AclUserVO> users = new ArrayList<>();
-        for (int page = 0; page < MAX_PAGES; page++) {
-            DescribeRoleListRequest request = new DescribeRoleListRequest();
-            request.setInstanceId(context.cloudInstanceId());
-            request.setOffset((long) page * PAGE_SIZE);
-            request.setLimit((long) PAGE_SIZE);
-            DescribeRoleListResponse response = 
clientFactory.call(context.credentialId(),
-                    context.regionId(), client -> 
client.DescribeRoleList(request));
+        long fetched = 0L;
+        for (long offset = 0L; ; offset += PAGE_SIZE) {
+            DescribeRoleListResponse response = describeRoles(context, offset);
             RoleItem[] data = response == null ? null : response.getData();
             if (data == null || data.length == 0) {
                 break;
             }
+            fetched += data.length;
             for (RoleItem role : data) {
                 if (role != null && StringUtils.hasText(role.getRoleName())) {
                     users.add(toUser(role, context.cloudInstanceId()));
                 }
             }
-            if (data.length < PAGE_SIZE) {
+            if (isLastRolePage(data.length, fetched, 
response.getTotalCount())) {
                 break;
             }
         }
@@ -94,17 +90,14 @@ public class TencentAclService {
         Context context = resolve(instanceId);
         String requestedPrincipal = StringUtils.hasText(principal) ? 
principal.trim() : null;
         List<AclRuleVO> rules = new ArrayList<>();
-        for (int page = 0; page < MAX_PAGES; page++) {
-            DescribeRoleListRequest request = new DescribeRoleListRequest();
-            request.setInstanceId(context.cloudInstanceId());
-            request.setOffset((long) page * PAGE_SIZE);
-            request.setLimit((long) PAGE_SIZE);
-            DescribeRoleListResponse response = 
clientFactory.call(context.credentialId(),
-                    context.regionId(), client -> 
client.DescribeRoleList(request));
+        long fetched = 0L;
+        for (long offset = 0L; ; offset += PAGE_SIZE) {
+            DescribeRoleListResponse response = describeRoles(context, offset);
             RoleItem[] data = response == null ? null : response.getData();
             if (data == null || data.length == 0) {
                 break;
             }
+            fetched += data.length;
             for (RoleItem role : data) {
                 if (role == null || !StringUtils.hasText(role.getRoleName())) {
                     continue;
@@ -115,7 +108,7 @@ public class TencentAclService {
                 }
                 rules.add(toRule(role));
             }
-            if (data.length < PAGE_SIZE) {
+            if (isLastRolePage(data.length, fetched, 
response.getTotalCount())) {
                 break;
             }
         }
@@ -178,29 +171,39 @@ public class TencentAclService {
     }
 
     private RoleItem findRole(Context context, String roleName) {
-        for (int page = 0; page < MAX_PAGES; page++) {
-            DescribeRoleListRequest request = new DescribeRoleListRequest();
-            request.setInstanceId(context.cloudInstanceId());
-            request.setOffset((long) page * PAGE_SIZE);
-            request.setLimit((long) PAGE_SIZE);
-            DescribeRoleListResponse response = 
clientFactory.call(context.credentialId(),
-                    context.regionId(), client -> 
client.DescribeRoleList(request));
+        long fetched = 0L;
+        for (long offset = 0L; ; offset += PAGE_SIZE) {
+            DescribeRoleListResponse response = describeRoles(context, offset);
             RoleItem[] data = response == null ? null : response.getData();
             if (data == null || data.length == 0) {
                 break;
             }
+            fetched += data.length;
             for (RoleItem role : data) {
                 if (role != null && roleName.equals(role.getRoleName())) {
                     return role;
                 }
             }
-            if (data.length < PAGE_SIZE) {
+            if (isLastRolePage(data.length, fetched, 
response.getTotalCount())) {
                 break;
             }
         }
         throw new BusinessException(404, "ACL user not found: " + roleName);
     }
 
+    private DescribeRoleListResponse describeRoles(Context context, long 
offset) {
+        DescribeRoleListRequest request = new DescribeRoleListRequest();
+        request.setInstanceId(context.cloudInstanceId());
+        request.setOffset(offset);
+        request.setLimit((long) PAGE_SIZE);
+        return clientFactory.call(context.credentialId(),
+                context.regionId(), client -> 
client.DescribeRoleList(request));
+    }
+
+    private static boolean isLastRolePage(int returned, long fetched, Long 
totalCount) {
+        return returned < PAGE_SIZE || totalCount != null && totalCount >= 0L 
&& fetched >= totalCount;
+    }
+
     public void deleteUser(String instanceId, String username) {
         Context context = resolve(instanceId);
         String roleName = requireRoleName(username, "ACL username");
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogService.java
index 886b907bc..744980ed7 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogService.java
@@ -43,7 +43,6 @@ import java.util.Locale;
 public class TencentCatalogService implements CloudCatalogProvider {
 
     static final int PAGE_SIZE = 100;
-    static final int MAX_PAGES = 100;
     static final List<CloudRegionVO> SUPPORTED_REGIONS = List.of(
             region("ap-guangzhou", "Guangzhou"),
             region("ap-shenzhen-fsi", "Shenzhen Finance"),
@@ -83,9 +82,9 @@ public class TencentCatalogService implements 
CloudCatalogProvider {
         requireId(credentialId, "credentialId");
         requireNonBlank(regionId, "regionId");
         List<CloudInstanceOptionVO> instances = new ArrayList<>();
-        for (int page = 0; page < MAX_PAGES; page++) {
+        for (long offset = 0L; ; offset += PAGE_SIZE) {
             DescribeInstanceListRequest request = new 
DescribeInstanceListRequest();
-            request.setOffset((long) page * PAGE_SIZE);
+            request.setOffset(offset);
             request.setLimit((long) PAGE_SIZE);
             DescribeInstanceListResponse response = 
clientFactory.call(credentialId, regionId,
                     client -> client.DescribeInstanceList(request));
@@ -102,13 +101,17 @@ public class TencentCatalogService implements 
CloudCatalogProvider {
                     instances.add(option);
                 }
             }
-            if (data.length < PAGE_SIZE) {
+            if (data.length < PAGE_SIZE || hasFetchedAll(offset, 
response.getTotalCount())) {
                 break;
             }
         }
         return instances;
     }
 
+    private static boolean hasFetchedAll(long offset, Long totalCount) {
+        return totalCount != null && totalCount >= 0L && offset + PAGE_SIZE >= 
totalCount;
+    }
+
     @Override
     public CloudInstanceDetailVO getCloudInstance(Long credentialId, String 
regionId, String cloudInstanceId) {
         requireId(credentialId, "credentialId");
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
index 79560ea4e..110834bfd 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProvider.java
@@ -276,6 +276,10 @@ public class TencentInstanceProvider implements 
InstanceProvider {
         return totalCount != null && totalCount >= 0L && offset + pageSize >= 
totalCount;
     }
 
+    private static boolean hasFetchedAll(long fetched, Long totalCount) {
+        return totalCount != null && totalCount >= 0L && fetched >= totalCount;
+    }
+
     private static PageResult<TopicVO> paginate(List<TopicVO> topics, int 
page, int pageSize) {
         int total = topics.size();
         long offset = Pagination.pageOffset(page, pageSize);
@@ -409,10 +413,10 @@ public class TencentInstanceProvider implements 
InstanceProvider {
     private List<ConsumerGroupVO> listConsumerGroups(String instanceId, String 
search, boolean enrichTimes) {
         Context context = resolve(instanceId);
         List<ConsumerGroupVO> groups = new ArrayList<>();
-        for (int page = 0; page < MAX_PAGES; page++) {
+        for (long offset = 0L; ; offset += PAGE_SIZE) {
             DescribeConsumerGroupListRequest request = new 
DescribeConsumerGroupListRequest();
             request.setInstanceId(context.cloudInstanceId());
-            request.setOffset((long) page * PAGE_SIZE);
+            request.setOffset(offset);
             request.setLimit((long) PAGE_SIZE);
             DescribeConsumerGroupListResponse response = 
clientFactory.call(context.credentialId(), context.regionId(),
                     client -> client.DescribeConsumerGroupList(request));
@@ -432,7 +436,7 @@ public class TencentInstanceProvider implements 
InstanceProvider {
                     groups.add(group);
                 }
             }
-            if (data.length < PAGE_SIZE) {
+            if (data.length < PAGE_SIZE || hasFetchedAll(offset, PAGE_SIZE, 
response.getTotalCount())) {
                 break;
             }
         }
@@ -935,11 +939,12 @@ public class TencentInstanceProvider implements 
InstanceProvider {
 
     private List<SubscriptionData> listTopicSubscriptionsByGroup(Context 
context, String groupName) {
         List<SubscriptionData> all = new ArrayList<>();
-        for (int page = 0; page < MAX_PAGES; page++) {
+        long fetched = 0L;
+        for (long offset = 0L; ; offset += PAGE_SIZE) {
             DescribeTopicListByGroupRequest request = new 
DescribeTopicListByGroupRequest();
             request.setInstanceId(context.cloudInstanceId());
             request.setConsumerGroup(groupName);
-            request.setOffset((long) page * PAGE_SIZE);
+            request.setOffset(offset);
             request.setLimit((long) PAGE_SIZE);
             DescribeTopicListByGroupResponse response = 
clientFactory.call(context.credentialId(), context.regionId(),
                     client -> client.DescribeTopicListByGroup(request));
@@ -947,8 +952,9 @@ public class TencentInstanceProvider implements 
InstanceProvider {
             if (data == null || data.length == 0) {
                 break;
             }
+            fetched += data.length;
             all.addAll(Arrays.asList(data));
-            if (data.length < PAGE_SIZE) {
+            if (data.length < PAGE_SIZE || hasFetchedAll(fetched, 
response.getTotalCount())) {
                 break;
             }
         }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
index 5b43f55c6..fb45c4936 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunInstanceProviderTest.java
@@ -224,6 +224,73 @@ class AliyunInstanceProviderTest {
         
assertThat(groups.get(0).getConsumeType()).isEqualTo(ConsumeType.CLUSTERING);
     }
 
+    @Test
+    void listConsumerGroupsShouldFetchExactlyFiveFullPagesTest() {
+        stubInstance();
+        stubCallThrough();
+        
when(asyncClient.listConsumerGroups(any(ListConsumerGroupsRequest.class))).thenAnswer(invocation
 -> {
+            ListConsumerGroupsRequest request = invocation.getArgument(0);
+            return CompletableFuture.completedFuture(groupsResponse(500L,
+                    groupIdsForPage(request.getPageNumber(), 
AliyunConverters.PAGE_SIZE)));
+        });
+
+        List<ConsumerGroupVO> groups = 
provider.listConsumerGroups(STUDIO_INSTANCE_ID, null);
+
+        assertThat(groups).hasSize(500);
+        ArgumentCaptor<ListConsumerGroupsRequest> captor =
+                ArgumentCaptor.forClass(ListConsumerGroupsRequest.class);
+        verify(asyncClient, times(5)).listConsumerGroups(captor.capture());
+        
assertThat(captor.getAllValues()).extracting(ListConsumerGroupsRequest::getPageNumber)
+                .containsExactly(1, 2, 3, 4, 5);
+    }
+
+    @Test
+    void listConsumerGroupsShouldTraversePastLegacyFivePageCapTest() {
+        stubInstance();
+        stubCallThrough();
+        
when(asyncClient.listConsumerGroups(any(ListConsumerGroupsRequest.class))).thenAnswer(invocation
 -> {
+            ListConsumerGroupsRequest request = invocation.getArgument(0);
+            int pageNumber = request.getPageNumber();
+            if (pageNumber <= 5) {
+                return CompletableFuture.completedFuture(groupsResponse(501L,
+                        groupIdsForPage(pageNumber, 
AliyunConverters.PAGE_SIZE)));
+            }
+            return CompletableFuture.completedFuture(groupsResponse(501L, 
"GID_500"));
+        });
+
+        List<ConsumerGroupVO> groups = 
provider.listConsumerGroups(STUDIO_INSTANCE_ID, null);
+
+        assertThat(groups).hasSize(501);
+        ArgumentCaptor<ListConsumerGroupsRequest> captor =
+                ArgumentCaptor.forClass(ListConsumerGroupsRequest.class);
+        verify(asyncClient, times(6)).listConsumerGroups(captor.capture());
+        
assertThat(captor.getAllValues()).extracting(ListConsumerGroupsRequest::getPageNumber)
+                .containsExactly(1, 2, 3, 4, 5, 6);
+    }
+
+    @Test
+    void listConsumerGroupsShouldStopOnShortPageWhenTotalCountIsMissingTest() {
+        stubInstance();
+        stubCallThrough();
+        
when(asyncClient.listConsumerGroups(any(ListConsumerGroupsRequest.class))).thenAnswer(invocation
 -> {
+            ListConsumerGroupsRequest request = invocation.getArgument(0);
+            if (request.getPageNumber() == 1) {
+                return CompletableFuture.completedFuture(groupsResponse(null,
+                        groupIdsForPage(1, AliyunConverters.PAGE_SIZE)));
+            }
+            return CompletableFuture.completedFuture(groupsResponse(null, 
"GID_100"));
+        });
+
+        List<ConsumerGroupVO> groups = 
provider.listConsumerGroups(STUDIO_INSTANCE_ID, null);
+
+        assertThat(groups).hasSize(101);
+        ArgumentCaptor<ListConsumerGroupsRequest> captor =
+                ArgumentCaptor.forClass(ListConsumerGroupsRequest.class);
+        verify(asyncClient, times(2)).listConsumerGroups(captor.capture());
+        
assertThat(captor.getAllValues()).extracting(ListConsumerGroupsRequest::getPageNumber)
+                .containsExactly(1, 2);
+    }
+
     @Test
     void getGroupProgressShouldMapLagRowsTest() {
         stubInstance();
@@ -736,6 +803,12 @@ class AliyunInstanceProviderTest {
                 .build();
     }
 
+    private static String[] groupIdsForPage(int pageNumber, int pageSize) {
+        return IntStream.range(0, pageSize)
+                .mapToObj(index -> "GID_" + ((pageNumber - 1) * pageSize + 
index))
+                .toArray(String[]::new);
+    }
+
     @Test
     void normalizeDeliveryOrderTypeShouldMapFifoToOrderlyTest() {
         org.junit.jupiter.api.Assertions.assertEquals("Orderly",
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentAclServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentAclServiceTest.java
index 4304752ff..482277c9b 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentAclServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentAclServiceTest.java
@@ -17,6 +17,7 @@
 package org.apache.rocketmq.studio.provider.tencent;
 
 import com.tencentcloudapi.trocket.v20230308.TrocketClient;
+import com.tencentcloudapi.trocket.v20230308.models.DescribeRoleListRequest;
 import com.tencentcloudapi.trocket.v20230308.models.DescribeRoleListResponse;
 import com.tencentcloudapi.trocket.v20230308.models.ModifyRoleRequest;
 import com.tencentcloudapi.trocket.v20230308.models.RoleItem;
@@ -34,6 +35,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.util.List;
 import java.util.Optional;
+import java.util.stream.IntStream;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -41,6 +43,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -110,6 +113,66 @@ class TencentAclServiceTest {
                 .isEqualTo("reader-role");
     }
 
+    @Test
+    void listUsersShouldFetchExactlyTenThousandTencentRolesTest() throws 
Exception {
+        when(client.DescribeRoleList(any())).thenAnswer(invocation -> {
+            DescribeRoleListRequest request = invocation.getArgument(0);
+            DescribeRoleListResponse response = new DescribeRoleListResponse();
+            response.setTotalCount(10000L);
+            response.setData(rolePage(request.getOffset(), request.getLimit(), 
10000));
+            return response;
+        });
+
+        assertThat(service.listUsers(INSTANCE_ID)).hasSize(10000);
+        verify(client, times(100)).DescribeRoleList(any());
+    }
+
+    @Test
+    void listRulesShouldFetchPastLegacyTenThousandTencentRoleCapTest() throws 
Exception {
+        when(client.DescribeRoleList(any())).thenAnswer(invocation -> {
+            DescribeRoleListRequest request = invocation.getArgument(0);
+            DescribeRoleListResponse response = new DescribeRoleListResponse();
+            response.setTotalCount(10001L);
+            response.setData(rolePage(request.getOffset(), request.getLimit(), 
10001));
+            return response;
+        });
+
+        assertThat(service.listRules(INSTANCE_ID, null)).hasSize(10001);
+        verify(client, times(101)).DescribeRoleList(any());
+    }
+
+    @Test
+    void updateUserShouldFindRolePastLegacyTenThousandTencentRoleCapTest() 
throws Exception {
+        when(client.DescribeRoleList(any())).thenAnswer(invocation -> {
+            DescribeRoleListRequest request = invocation.getArgument(0);
+            DescribeRoleListResponse response = new DescribeRoleListResponse();
+            response.setTotalCount(10001L);
+            response.setData(rolePage(request.getOffset(), request.getLimit(), 
10001));
+            return response;
+        });
+
+        AclUserVO updated = service.updateUser(INSTANCE_ID, AclUserVO.builder()
+                .username("role-10000")
+                .build());
+
+        assertThat(updated.getUsername()).isEqualTo("role-10000");
+        verify(client, times(101)).DescribeRoleList(any());
+        verify(client).ModifyRole(any());
+    }
+
+    @Test
+    void listUsersShouldStopOnShortPageWhenTencentTotalCountIsMissingTest() 
throws Exception {
+        when(client.DescribeRoleList(any())).thenAnswer(invocation -> {
+            DescribeRoleListRequest request = invocation.getArgument(0);
+            DescribeRoleListResponse response = new DescribeRoleListResponse();
+            response.setData(rolePage(request.getOffset(), request.getLimit(), 
150));
+            return response;
+        });
+
+        assertThat(service.listUsers(INSTANCE_ID)).hasSize(150);
+        verify(client, times(2)).DescribeRoleList(any());
+    }
+
     @Test
     void createUserShouldRejectNullPayloadTest() {
         assertThatThrownBy(() -> service.createUser(INSTANCE_ID, null))
@@ -211,4 +274,21 @@ class TencentAclServiceTest {
                 .hasMessage("Tencent Cloud roles only support ALLOW ACL rules")
                 .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(400));
     }
+
+    private static RoleItem[] rolePage(Long offset, Long limit, int total) {
+        int start = offset == null ? 0 : offset.intValue();
+        int size = Math.min(limit == null ? TencentAclService.PAGE_SIZE : 
limit.intValue(),
+                Math.max(total - start, 0));
+        return IntStream.range(0, size)
+                .mapToObj(index -> role("role-" + (start + index)))
+                .toArray(RoleItem[]::new);
+    }
+
+    private static RoleItem role(String name) {
+        RoleItem role = new RoleItem();
+        role.setRoleName(name);
+        role.setPermRead(true);
+        role.setPermWrite(false);
+        return role;
+    }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogServiceTest.java
index 1d4714a1d..03d15b621 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogServiceTest.java
@@ -17,9 +17,11 @@
 package org.apache.rocketmq.studio.provider.tencent;
 
 import 
com.tencentcloudapi.trocket.v20230308.models.DescribeInstanceListResponse;
+import 
com.tencentcloudapi.trocket.v20230308.models.DescribeInstanceListRequest;
 import com.tencentcloudapi.trocket.v20230308.models.DescribeInstanceResponse;
 import com.tencentcloudapi.trocket.v20230308.models.Endpoint;
 import com.tencentcloudapi.trocket.v20230308.models.InstanceItem;
+import com.tencentcloudapi.trocket.v20230308.TrocketClient;
 import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
 import org.apache.rocketmq.studio.provider.CloudInstanceOptionVO;
 import org.apache.rocketmq.studio.provider.CloudRegionVO;
@@ -34,6 +36,9 @@ import java.util.List;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
@@ -45,11 +50,18 @@ class TencentCatalogServiceTest {
     @Mock
     private TencentClientFactory clientFactory;
 
+    @Mock
+    private TrocketClient client;
+
     private TencentCatalogService service;
 
     @BeforeEach
     void setUp() {
         service = new TencentCatalogService(clientFactory);
+        lenient().when(clientFactory.call(eq(CREDENTIAL_ID), eq(REGION), 
any())).thenAnswer(invocation -> {
+            TencentClientFactory.TencentCall<Object> action = 
invocation.getArgument(2);
+            return action.execute(client);
+        });
     }
 
     @Test
@@ -121,6 +133,48 @@ class TencentCatalogServiceTest {
                 .isEqualTo("rmq-valid");
     }
 
+    @Test
+    void 
listCloudInstancesShouldContinuePastTenThousandRecordsWhenTotalCountRequiresItTest()
 throws Exception {
+        InstanceItem item = new InstanceItem();
+        item.setInstanceId("rmq-page");
+        item.setInstanceName("page");
+        when(client.DescribeInstanceList(any())).thenAnswer(invocation -> {
+            DescribeInstanceListRequest request = invocation.getArgument(0);
+            DescribeInstanceListResponse response = new 
DescribeInstanceListResponse();
+            response.setTotalCount(10_001L);
+            response.setData(request.getOffset() < 10_000L
+                    ? java.util.stream.IntStream.range(0, 100).mapToObj(index 
-> item).toArray(InstanceItem[]::new)
+                    : new InstanceItem[]{item});
+            return response;
+        });
+
+        List<CloudInstanceOptionVO> instances = 
service.listCloudInstances(CREDENTIAL_ID, REGION, null);
+
+        assertThat(instances).hasSize(10_001);
+        org.mockito.ArgumentCaptor<DescribeInstanceListRequest> captor =
+                
org.mockito.ArgumentCaptor.forClass(DescribeInstanceListRequest.class);
+        verify(client, times(101)).DescribeInstanceList(captor.capture());
+        
assertThat(captor.getAllValues().get(100).getOffset()).isEqualTo(10_000L);
+    }
+
+    @Test
+    void 
listCloudInstancesShouldStopAtExactlyTenThousandRecordsWhenTotalCountIsReachedTest()
 throws Exception {
+        InstanceItem item = new InstanceItem();
+        item.setInstanceId("rmq-page");
+        item.setInstanceName("page");
+        when(client.DescribeInstanceList(any())).thenAnswer(invocation -> {
+            DescribeInstanceListResponse response = new 
DescribeInstanceListResponse();
+            response.setTotalCount(10_000L);
+            response.setData(java.util.stream.IntStream.range(0, 100)
+                    .mapToObj(index -> item)
+                    .toArray(InstanceItem[]::new));
+            return response;
+        });
+
+        assertThat(service.listCloudInstances(CREDENTIAL_ID, REGION, 
null)).hasSize(10_000);
+        verify(client, times(100)).DescribeInstanceList(any());
+    }
+
     @Test
     void getCloudInstanceShouldMapOnlyOpenEndpointsTest() {
         Endpoint vpc = endpoint("VPC", "OPEN", "vpc.tencent:8080");
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
index 234584860..203578989 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentInstanceProviderTest.java
@@ -30,6 +30,7 @@ import 
com.tencentcloudapi.trocket.v20230308.models.DescribeMessageTraceRequest;
 import 
com.tencentcloudapi.trocket.v20230308.models.DescribeMessageTraceResponse;
 import com.tencentcloudapi.trocket.v20230308.models.MessageItem;
 import com.tencentcloudapi.trocket.v20230308.models.MessageTraceItem;
+import 
com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListByGroupRequest;
 import 
com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListByGroupResponse;
 import 
com.tencentcloudapi.trocket.v20230308.models.DescribeConsumerGroupListRequest;
 import com.tencentcloudapi.trocket.v20230308.models.DescribeTopicListRequest;
@@ -537,6 +538,28 @@ class TencentInstanceProviderTest {
         assertThat(groups.get(0).getInstances()).isNotNull().isEmpty();
     }
 
+    @Test
+    void 
listConsumerGroupsShouldContinuePastTenThousandRecordsWhenTotalCountRequiresItTest()
 throws Exception {
+        ConsumeGroupItem item = new ConsumeGroupItem();
+        item.setConsumerGroup("GID_page");
+        when(client.DescribeConsumerGroupList(any())).thenAnswer(invocation -> 
{
+            DescribeConsumerGroupListRequest request = 
invocation.getArgument(0);
+            DescribeConsumerGroupListResponse response = new 
DescribeConsumerGroupListResponse();
+            response.setTotalCount(10_001L);
+            response.setData(request.getOffset() < 10_000L
+                    ? IntStream.range(0, 100).mapToObj(index -> 
item).toArray(ConsumeGroupItem[]::new)
+                    : new ConsumeGroupItem[]{item});
+            return response;
+        });
+
+        assertThat(provider.listConsumerGroups(STUDIO_INSTANCE_ID, 
"does-not-match")).isEmpty();
+
+        ArgumentCaptor<DescribeConsumerGroupListRequest> captor =
+                
ArgumentCaptor.forClass(DescribeConsumerGroupListRequest.class);
+        verify(client, times(101)).DescribeConsumerGroupList(captor.capture());
+        
assertThat(captor.getAllValues().get(100).getOffset()).isEqualTo(10_000L);
+    }
+
     @Test
     void createConsumerGroupShouldCallTencentOpenApiTest() throws Exception {
         when(client.CreateConsumerGroup(any())).thenReturn(null);
@@ -614,6 +637,47 @@ class TencentInstanceProviderTest {
         
assertThat(preview.getQueues().get(0).getRiskLevel()).isEqualTo("WARNING");
     }
 
+    @Test
+    void 
getGroupSubscriptionsShouldFetchExactlyTenThousandTencentSubscriptionsTest() 
throws Exception {
+        when(client.DescribeTopicListByGroup(any())).thenAnswer(invocation -> {
+            DescribeTopicListByGroupRequest request = 
invocation.getArgument(0);
+            DescribeTopicListByGroupResponse response = new 
DescribeTopicListByGroupResponse();
+            response.setTotalCount(10000L);
+            response.setData(subscriptionPage(request.getOffset(), 
request.getLimit(), 10000));
+            return response;
+        });
+
+        assertThat(provider.getGroupSubscriptions(STUDIO_INSTANCE_ID, 
"GID_test")).hasSize(10000);
+        verify(client, times(100)).DescribeTopicListByGroup(any());
+    }
+
+    @Test
+    void 
getGroupProgressShouldFetchPastLegacyTenThousandTencentSubscriptionCapTest() 
throws Exception {
+        when(client.DescribeTopicListByGroup(any())).thenAnswer(invocation -> {
+            DescribeTopicListByGroupRequest request = 
invocation.getArgument(0);
+            DescribeTopicListByGroupResponse response = new 
DescribeTopicListByGroupResponse();
+            response.setTotalCount(10001L);
+            response.setData(subscriptionPage(request.getOffset(), 
request.getLimit(), 10001));
+            return response;
+        });
+
+        assertThat(provider.getGroupProgress(STUDIO_INSTANCE_ID, 
"GID_test")).hasSize(10001);
+        verify(client, times(101)).DescribeTopicListByGroup(any());
+    }
+
+    @Test
+    void 
getGroupSubscriptionsShouldStopOnShortPageWhenTencentTotalCountIsMissingTest() 
throws Exception {
+        when(client.DescribeTopicListByGroup(any())).thenAnswer(invocation -> {
+            DescribeTopicListByGroupRequest request = 
invocation.getArgument(0);
+            DescribeTopicListByGroupResponse response = new 
DescribeTopicListByGroupResponse();
+            response.setData(subscriptionPage(request.getOffset(), 
request.getLimit(), 150));
+            return response;
+        });
+
+        assertThat(provider.getGroupSubscriptions(STUDIO_INSTANCE_ID, 
"GID_test")).hasSize(150);
+        verify(client, times(2)).DescribeTopicListByGroup(any());
+    }
+
     @Test
     void resetOffsetShouldCallTencentOpenApiTest() throws Exception {
         when(client.ResetConsumerGroupOffset(any())).thenReturn(null);
@@ -645,6 +709,22 @@ class TencentInstanceProviderTest {
         return subscription;
     }
 
+    private static SubscriptionData[] subscriptionPage(Long offset, Long 
limit, int total) {
+        int start = offset == null ? 0 : offset.intValue();
+        int size = Math.min(limit == null ? TencentInstanceProvider.PAGE_SIZE 
: limit.intValue(),
+                Math.max(total - start, 0));
+        return IntStream.range(0, size)
+                .mapToObj(index -> {
+                    SubscriptionData subscription = subscription("GID_test");
+                    subscription.setTopic("topic-" + (start + index));
+                    subscription.setSubString("*");
+                    subscription.setExpressionType("TAG");
+                    subscription.setConsumerLag((long) start + index);
+                    return subscription;
+                })
+                .toArray(SubscriptionData[]::new);
+    }
+
     @Test
     void queryMessagesByMsgIdShouldReturnDetailWithBodyTest() throws Exception 
{
         DescribeMessageResponse detail = new DescribeMessageResponse();

Reply via email to