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 c7eda0a0b feat(credential): conflict-safe creation and paginated 
inventory (#2306)
c7eda0a0b is described below

commit c7eda0a0beb107c0caf6eaaa881b7a0da9c60f94
Author: aias00 <[email protected]>
AuthorDate: Wed Aug 19 17:33:48 2026 +0800

    feat(credential): conflict-safe creation and paginated inventory (#2306)
    
    * fix: report cloud credential create conflicts
    
    * feat: paginate cloud credential listings
    
    * fix: keep cloud credential pagination contract consistent
    
    Signed-off-by: liuhy <[email protected]>
    
    * fix: consume paginated cloud credential results
    
    * fix: sort cloud credential pages by gmt_modified instead of updated_at
    
    ---------
    
    Signed-off-by: liuhy <[email protected]>
    Co-authored-by: lizhimins <[email protected]>
---
 .../credential/CloudCredentialController.java      |  10 +-
 .../credential/CloudCredentialRepository.java      |   2 +
 .../credential/CloudCredentialService.java         |  16 ++-
 .../MybatisPlusCloudCredentialRepository.java      |   7 ++
 .../credential/CloudCredentialServiceTest.java     |  16 +++
 web/src/api/cloudCredential.test.ts                |  29 +++--
 web/src/api/cloudCredential.ts                     |  18 ++-
 .../pages/instance/__tests__/InstancePage.test.tsx | 132 ++++++++++++---------
 web/src/pages/instance/index.tsx                   |   6 +-
 web/src/pages/settings/CloudCredentialTab.tsx      |   4 +-
 .../settings/__tests__/CloudCredentialTab.test.tsx |  29 +++--
 11 files changed, 176 insertions(+), 93 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialController.java
index e33128954..e657df96c 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialController.java
@@ -19,6 +19,8 @@ package org.apache.rocketmq.studio.provider.credential;
 import jakarta.validation.Valid;
 import org.apache.rocketmq.studio.common.domain.DeleteRequestDTO;
 import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.common.util.EntityIds;
 import org.springframework.http.CacheControl;
@@ -28,10 +30,10 @@ import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import lombok.RequiredArgsConstructor;
 
-import java.util.List;
 
 @RequiredArgsConstructor
 @RestController
@@ -41,8 +43,10 @@ public class CloudCredentialController {
     private final CloudCredentialService credentialService;
 
     @GetMapping
-    public Result<List<CloudCredentialVO>> listCredentials() {
-        return Result.ok(credentialService.listMasked());
+    public Result<PageResult<CloudCredentialVO>> 
listCredentials(@RequestParam(required = false) InstanceVendor vendor,
+            @RequestParam(required = false) String search, 
@RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "20") int pageSize) {
+        return Result.ok(credentialService.listMasked(vendor, search, page, 
pageSize));
     }
 
     @PostMapping("/create")
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialRepository.java
index 9e76d9f7c..44a03085f 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialRepository.java
@@ -17,6 +17,7 @@
 package org.apache.rocketmq.studio.provider.credential;
 
 import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
+import org.apache.rocketmq.studio.common.domain.PageResult;
 
 import java.util.List;
 import java.util.Optional;
@@ -24,6 +25,7 @@ import java.util.Optional;
 public interface CloudCredentialRepository {
 
     List<CloudCredentialVO> findAll();
+    PageResult<CloudCredentialVO> findPage(InstanceVendor vendor, String 
search, int page, int pageSize);
 
     Optional<CloudCredentialVO> findById(Long id);
 
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialService.java
index b0ad47bde..e8e6d6d71 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialService.java
@@ -19,12 +19,15 @@ package org.apache.rocketmq.studio.provider.credential;
 import org.springframework.util.StringUtils;
 
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.util.CredentialUtils;
 import org.apache.rocketmq.studio.audit.OperationAuditService;
 import org.apache.rocketmq.studio.instance.InstanceRepository;
 import org.apache.rocketmq.studio.provider.alibaba.AliyunClientFactory;
 import org.apache.rocketmq.studio.provider.tencent.TencentClientFactory;
 import org.springframework.stereotype.Service;
+import org.springframework.dao.DuplicateKeyException;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 
@@ -48,6 +51,11 @@ public class CloudCredentialService {
                 .map(this::maskAccessKey)
                 .toList();
     }
+    public PageResult<CloudCredentialVO> listMasked(InstanceVendor vendor, 
String search, int page, int pageSize) {
+        if (page < 1 || pageSize < 1 || pageSize > 100) throw new 
BusinessException(400, "Invalid page or pageSize");
+        PageResult<CloudCredentialVO> result = 
credentialRepository.findPage(vendor, search, page, pageSize);
+        return 
PageResult.of(result.getItems().stream().map(this::maskAccessKey).toList(), 
result.getTotal(), page, pageSize);
+    }
 
     public CloudCredentialVO create(CloudCredentialVO credential) {
         if (credential == null) {
@@ -71,7 +79,13 @@ public class CloudCredentialService {
         log.info("Creating cloud credential name={}, vendor={}", 
credential.getName(), credential.getVendor());
         credential.setGmtCreate(LocalDateTime.now());
         credential.setGmtModified(LocalDateTime.now());
-        CloudCredentialVO saved = credentialRepository.save(credential);
+        CloudCredentialVO saved;
+        try {
+            saved = credentialRepository.save(credential);
+        } catch (DuplicateKeyException exception) {
+            throw new BusinessException(409, "Cloud credential already exists 
for vendor "
+                    + credential.getVendor() + " and accessKey " + 
CredentialUtils.mask(credential.getAccessKey()));
+        }
         recordAudit("CREATE_CLOUD_CREDENTIAL", "CLOUD_CREDENTIAL", 
String.valueOf(saved.getId()), null,
                 credentialAuditDetail(saved));
         return maskAccessKey(saved);
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/MybatisPlusCloudCredentialRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/MybatisPlusCloudCredentialRepository.java
index 36e555e49..f60e42251 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/MybatisPlusCloudCredentialRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/MybatisPlusCloudCredentialRepository.java
@@ -17,6 +17,8 @@
 package org.apache.rocketmq.studio.provider.credential;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.rocketmq.studio.common.domain.PageResult;
 import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.common.util.CredentialUtils;
@@ -47,6 +49,11 @@ public class MybatisPlusCloudCredentialRepository implements 
CloudCredentialRepo
                 .map(MybatisPlusCloudCredentialRepository::toVO)
                 .collect(Collectors.toList());
     }
+    @Override public PageResult<CloudCredentialVO> findPage(InstanceVendor 
vendor, String search, int page, int pageSize) {
+        QueryWrapper<RmqCloudCredential> q = new 
QueryWrapper<RmqCloudCredential>().eq(vendor != null, "vendor", vendor == null 
? null : vendor.name()).like(search != null && !search.isBlank(), "name", 
search).orderByDesc("gmt_modified", "id");
+        Page<RmqCloudCredential> result = credentialMapper.selectPage(new 
Page<>(page, pageSize), q);
+        return 
PageResult.of(result.getRecords().stream().map(MybatisPlusCloudCredentialRepository::toVO).toList(),
 result.getTotal(), page, pageSize);
+    }
 
     @Override
     public Optional<CloudCredentialVO> findById(Long id) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialServiceTest.java
index 89d248f98..9d84b129c 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialServiceTest.java
@@ -28,6 +28,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.dao.DuplicateKeyException;
 
 import java.util.List;
 import java.util.Optional;
@@ -105,6 +106,21 @@ class CloudCredentialServiceTest {
         verify(credentialRepository, never()).save(any());
     }
 
+    @Test
+    void createShouldTranslateConcurrentDuplicateKeyToConflict() {
+        CloudCredentialVO request = new CloudCredentialVO();
+        request.setName("race");
+        request.setVendor(InstanceVendor.ALIYUN);
+        request.setAccessKey("LTAI5tRaceKey00000000001");
+        request.setSecretKey("sk");
+        
when(credentialRepository.findByVendorAndAccessKey(InstanceVendor.ALIYUN, 
request.getAccessKey()))
+                .thenReturn(Optional.empty());
+        when(credentialRepository.save(any())).thenThrow(new 
DuplicateKeyException("duplicate"));
+
+        assertThatThrownBy(() -> 
service.create(request)).isInstanceOf(BusinessException.class)
+                .satisfies(error -> assertThat(((BusinessException) 
error).getCode()).isEqualTo(409));
+    }
+
     @Test
     void createShouldRejectApacheVendorTest() {
         CloudCredentialVO request = new CloudCredentialVO();
diff --git a/web/src/api/cloudCredential.test.ts 
b/web/src/api/cloudCredential.test.ts
index 162bfc48a..ea380f31a 100644
--- a/web/src/api/cloudCredential.test.ts
+++ b/web/src/api/cloudCredential.test.ts
@@ -41,22 +41,27 @@ describe('cloudCredential API', () => {
   it('returns masked credentials from the backend', async () => {
     mock.onGet('/cloud-credentials').reply(200, {
       code: 200,
-      data: [
-        {
-          id: 'cred-1',
-          name: 'aliyun-test',
-          vendor: 'ALIYUN',
-          accessKey: 'LTAI****0001',
-          createdAt: '2026-08-06T00:00:00Z',
-        },
-      ],
+      data: {
+        items: [
+          {
+            id: 'cred-1',
+            name: 'aliyun-test',
+            vendor: 'ALIYUN',
+            accessKey: 'LTAI****0001',
+            createdAt: '2026-08-06T00:00:00Z',
+          },
+        ],
+        total: 1,
+        page: 1,
+        size: 20,
+      },
     });
 
     const credentials = await listCloudCredentials();
 
-    expect(credentials).toHaveLength(1);
-    expect(credentials[0].vendor).toBe('ALIYUN');
-    expect(credentials[0].secretKey).toBeUndefined();
+    expect(credentials.items).toHaveLength(1);
+    expect(credentials.items[0].vendor).toBe('ALIYUN');
+    expect(credentials.items[0].secretKey).toBeUndefined();
   });
 
   it('creates a credential with the full payload', async () => {
diff --git a/web/src/api/cloudCredential.ts b/web/src/api/cloudCredential.ts
index 39890c43d..e08a357e2 100644
--- a/web/src/api/cloudCredential.ts
+++ b/web/src/api/cloudCredential.ts
@@ -16,8 +16,22 @@ export interface CloudCredential {
   gmtCreate: string;
 }
 
-export async function listCloudCredentials() {
-  const res = await client.get<{ data: CloudCredential[] 
}>('/cloud-credentials');
+export interface CloudCredentialPage {
+  items: CloudCredential[];
+  total: number;
+  page: number;
+  size: number;
+}
+
+export async function listCloudCredentials(
+  vendor?: InstanceVendor,
+  search?: string,
+  page = 1,
+  pageSize = 20,
+): Promise<CloudCredentialPage> {
+  const res = await client.get<{ data: CloudCredentialPage 
}>('/cloud-credentials', {
+    params: { vendor, search, page, pageSize },
+  });
   return res.data.data;
 }
 
diff --git a/web/src/pages/instance/__tests__/InstancePage.test.tsx 
b/web/src/pages/instance/__tests__/InstancePage.test.tsx
index 0558fd42e..a2f60a103 100644
--- a/web/src/pages/instance/__tests__/InstancePage.test.tsx
+++ b/web/src/pages/instance/__tests__/InstancePage.test.tsx
@@ -22,6 +22,7 @@ import { MemoryRouter } from 'react-router-dom';
 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
 import * as aliyunCatalogApi from '../../../api/aliyunCatalog';
 import * as cloudCredentialApi from '../../../api/cloudCredential';
+import type { CloudCredential, CloudCredentialPage } from 
'../../../api/cloudCredential';
 import type { Instance } from '../../../api/instance';
 import { LangProvider } from '../../../i18n/LangContext';
 import * as instanceService from '../../../services/instanceService';
@@ -48,6 +49,13 @@ vi.mock('../../../services/instanceService', () => ({
   updateInstance: vi.fn(),
 }));
 
+const cloudCredentialPage = (items: CloudCredential[]): CloudCredentialPage => 
({
+  items,
+  total: items.length,
+  page: 1,
+  size: 20,
+});
+
 beforeAll(() => {
   Object.defineProperty(window, 'matchMedia', {
     writable: true,
@@ -105,7 +113,7 @@ const deferred = <T,>() => {
 describe('InstancePage', () => {
   beforeEach(() => {
     vi.clearAllMocks();
-    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([]);
+    
vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue(cloudCredentialPage([]));
     vi.mocked(aliyunCatalogApi.listAliyunRegions).mockResolvedValue([]);
     vi.mocked(aliyunCatalogApi.listAliyunInstances).mockResolvedValue([]);
     vi.mocked(instanceService.listInstances).mockResolvedValue([
@@ -394,22 +402,24 @@ describe('InstancePage', () => {
     const user = userEvent.setup();
     const oldRegions = deferred<Array<{ regionId: string; regionName: string 
}>>();
     const latestRegions = deferred<Array<{ regionId: string; regionName: 
string }>>();
-    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([
-      {
-        id: 101,
-        name: 'old-account',
-        vendor: 'ALIYUN',
-        accessKey: 'LTAI-old',
-        gmtCreate: '2026-01-01T00:00:00Z',
-      },
-      {
-        id: 102,
-        name: 'latest-account',
-        vendor: 'ALIYUN',
-        accessKey: 'LTAI-latest',
-        gmtCreate: '2026-01-01T00:00:00Z',
-      },
-    ]);
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue(
+      cloudCredentialPage([
+        {
+          id: 101,
+          name: 'old-account',
+          vendor: 'ALIYUN',
+          accessKey: 'LTAI-old',
+          gmtCreate: '2026-01-01T00:00:00Z',
+        },
+        {
+          id: 102,
+          name: 'latest-account',
+          vendor: 'ALIYUN',
+          accessKey: 'LTAI-latest',
+          gmtCreate: '2026-01-01T00:00:00Z',
+        },
+      ]),
+    );
     vi.mocked(aliyunCatalogApi.listAliyunRegions)
       .mockReturnValueOnce(oldRegions.promise)
       .mockReturnValueOnce(latestRegions.promise);
@@ -461,15 +471,17 @@ describe('InstancePage', () => {
       deferred<
         Array<{ instanceId: string; instanceName: string; status: string; 
regionId: string }>
       >();
-    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([
-      {
-        id: 103,
-        name: 'cloud-account',
-        vendor: 'ALIYUN',
-        accessKey: 'LTAI-one',
-        gmtCreate: '2026-01-01T00:00:00Z',
-      },
-    ]);
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue(
+      cloudCredentialPage([
+        {
+          id: 103,
+          name: 'cloud-account',
+          vendor: 'ALIYUN',
+          accessKey: 'LTAI-one',
+          gmtCreate: '2026-01-01T00:00:00Z',
+        },
+      ]),
+    );
     vi.mocked(aliyunCatalogApi.listAliyunRegions).mockResolvedValue([
       { regionId: 'cn-beijing', regionName: 'Beijing' },
       { regionId: 'cn-shanghai', regionName: 'Shanghai' },
@@ -541,22 +553,24 @@ describe('InstancePage', () => {
   it('clears a pending region load when the cloud vendor changes', async () => 
{
     const user = userEvent.setup();
     const pendingRegions = deferred<Array<{ regionId: string; regionName: 
string }>>();
-    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([
-      {
-        id: 104,
-        name: 'aliyun-account',
-        vendor: 'ALIYUN',
-        accessKey: 'LTAI-one',
-        gmtCreate: '2026-01-01T00:00:00Z',
-      },
-      {
-        id: 105,
-        name: 'tencent-account',
-        vendor: 'TENCENT',
-        accessKey: 'AKID-one',
-        gmtCreate: '2026-01-01T00:00:00Z',
-      },
-    ]);
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue(
+      cloudCredentialPage([
+        {
+          id: 104,
+          name: 'aliyun-account',
+          vendor: 'ALIYUN',
+          accessKey: 'LTAI-one',
+          gmtCreate: '2026-01-01T00:00:00Z',
+        },
+        {
+          id: 105,
+          name: 'tencent-account',
+          vendor: 'TENCENT',
+          accessKey: 'AKID-one',
+          gmtCreate: '2026-01-01T00:00:00Z',
+        },
+      ]),
+    );
     
vi.mocked(aliyunCatalogApi.listAliyunRegions).mockReturnValue(pendingRegions.promise);
 
     renderPage();
@@ -600,22 +614,24 @@ describe('InstancePage', () => {
       deferred<
         Array<{ instanceId: string; instanceName: string; status: string; 
regionId: string }>
       >();
-    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue([
-      {
-        id: 104,
-        name: 'aliyun-account',
-        vendor: 'ALIYUN',
-        accessKey: 'LTAI-one',
-        gmtCreate: '2026-01-01T00:00:00Z',
-      },
-      {
-        id: 105,
-        name: 'tencent-account',
-        vendor: 'TENCENT',
-        accessKey: 'AKID-one',
-        gmtCreate: '2026-01-01T00:00:00Z',
-      },
-    ]);
+    vi.mocked(cloudCredentialApi.listCloudCredentials).mockResolvedValue(
+      cloudCredentialPage([
+        {
+          id: 104,
+          name: 'aliyun-account',
+          vendor: 'ALIYUN',
+          accessKey: 'LTAI-one',
+          gmtCreate: '2026-01-01T00:00:00Z',
+        },
+        {
+          id: 105,
+          name: 'tencent-account',
+          vendor: 'TENCENT',
+          accessKey: 'AKID-one',
+          gmtCreate: '2026-01-01T00:00:00Z',
+        },
+      ]),
+    );
     vi.mocked(aliyunCatalogApi.listAliyunRegions).mockResolvedValue([
       { regionId: 'cn-beijing', regionName: 'Beijing' },
     ]);
diff --git a/web/src/pages/instance/index.tsx b/web/src/pages/instance/index.tsx
index 63fbd7643..547f5a61e 100644
--- a/web/src/pages/instance/index.tsx
+++ b/web/src/pages/instance/index.tsx
@@ -178,10 +178,10 @@ const InstancePage = () => {
     let active = true;
     const timer = window.setTimeout(() => {
       setCredentialsLoading(true);
-      listCloudCredentials()
-        .then((items) => {
+      listCloudCredentials(vendor)
+        .then((result) => {
           if (active) {
-            setCredentials(items.filter((item) => item.vendor === vendor));
+            setCredentials(result.items);
           }
         })
         .catch(() => {
diff --git a/web/src/pages/settings/CloudCredentialTab.tsx 
b/web/src/pages/settings/CloudCredentialTab.tsx
index 476bee009..2cc682f4d 100644
--- a/web/src/pages/settings/CloudCredentialTab.tsx
+++ b/web/src/pages/settings/CloudCredentialTab.tsx
@@ -76,8 +76,8 @@ export const CloudCredentialTab = () => {
   useEffect(() => {
     let cancelled = false;
     void listCloudCredentials()
-      .then((list) => {
-        if (!cancelled) setCredentials(list);
+      .then((result) => {
+        if (!cancelled) setCredentials(result.items);
       })
       .catch(() => {
         if (!cancelled) message.error('云凭据加载失败,请稍后重试');
diff --git a/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx 
b/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
index 46ab48c99..47213b993 100644
--- a/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
+++ b/web/src/pages/settings/__tests__/CloudCredentialTab.test.tsx
@@ -19,7 +19,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } 
from 'vitest';
 import { render, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
-import type { CloudCredential } from '../../../api/cloudCredential';
+import type { CloudCredentialPage } from '../../../api/cloudCredential';
 import {
   createCloudCredential,
   deleteCloudCredential,
@@ -36,16 +36,21 @@ vi.mock('../../../api/cloudCredential', () => ({
   updateCloudCredential: vi.fn(),
 }));
 
-const credentials: CloudCredential[] = [
-  {
-    id: 1,
-    name: 'aliyun-test',
-    vendor: 'ALIYUN',
-    accessKey: 'LTAI****0001',
-    remark: '测试账号',
-    gmtCreate: '2026-08-18T10:00:00',
-  },
-];
+const credentials: CloudCredentialPage = {
+  items: [
+    {
+      id: 1,
+      name: 'aliyun-test',
+      vendor: 'ALIYUN',
+      accessKey: 'LTAI****0001',
+      remark: '测试账号',
+      gmtCreate: '2026-08-18T10:00:00',
+    },
+  ],
+  total: 1,
+  page: 1,
+  size: 20,
+};
 
 const renderTab = () =>
   render(
@@ -123,7 +128,7 @@ describe('CloudCredentialTab', () => {
 
   it('updates name and remark while keeping the secret unchanged when blank', 
async () => {
     vi.mocked(updateCloudCredential).mockResolvedValue({
-      ...credentials[0],
+      ...credentials.items[0],
       name: 'aliyun-renamed',
       remark: '新备注',
     });

Reply via email to