From fab233abf636fcb7e30c94a4dea456bd4513a18c Mon Sep 17 00:00:00 2001 From: liuhy Date: Sun, 26 Jul 2026 21:23:23 -0700 Subject: [PATCH] [Studio] Harden cluster config and store loading --- .../studio/cluster/broker/ClusterService.java | 10 ++- .../cluster/broker/ClusterServiceTest.java | 17 ++++ web/src/stores/clusterStore.test.ts | 84 +++++++++++++++++++ web/src/stores/clusterStore.ts | 15 ++-- 4 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 web/src/stores/clusterStore.test.ts diff --git a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java index 544e3c80..158bdfc5 100644 --- a/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java +++ b/server/src/main/java/com/rocketmq/studio/cluster/broker/ClusterService.java @@ -65,7 +65,7 @@ public ClusterVO updateClusterConfig(UpdateConfigDTO command) { } if (command.getFlushDiskType() != null) { - config.setFlushDiskType(FlushDiskType.valueOf(command.getFlushDiskType())); + config.setFlushDiskType(parseFlushDiskType(command.getFlushDiskType())); } if (command.getAutoCreateTopicEnable() != null) { config.setAutoCreateTopicEnable(command.getAutoCreateTopicEnable()); @@ -95,6 +95,14 @@ public ClusterVO updateClusterConfig(UpdateConfigDTO command) { return cluster; } + private FlushDiskType parseFlushDiskType(String value) { + try { + return FlushDiskType.valueOf(value); + } catch (IllegalArgumentException ex) { + throw new BusinessException(400, "Invalid flushDiskType: " + value); + } + } + public boolean restartBroker(String clusterId, String brokerName) { log.info("Restarting broker: {} in cluster: {}", brokerName, clusterId); clusterRepository.findById(clusterId) diff --git a/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java index a476f1a1..91336e8d 100644 --- a/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java +++ b/server/src/test/java/com/rocketmq/studio/cluster/broker/ClusterServiceTest.java @@ -39,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -210,6 +211,22 @@ void updateConfigShouldThrowWhenClusterNotFound() { .hasMessageContaining("Cluster not found: missing"); } + @Test + void updateConfigShouldRejectInvalidFlushDiskType() { + when(clusterRepository.findById("cluster-1")).thenReturn(Optional.of(sampleCluster)); + + UpdateConfigDTO command = UpdateConfigDTO.builder() + .id("cluster-1") + .flushDiskType("INVALID_FLUSH") + .build(); + + assertThatThrownBy(() -> clusterService.updateClusterConfig(command)) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("Invalid flushDiskType: INVALID_FLUSH") + .satisfies(ex -> assertThat(((BusinessException) ex).getCode()).isEqualTo(400)); + verify(clusterRepository, never()).updateConfig(eq("cluster-1"), any(ClusterConfigVO.class)); + } + @Test void updateConfigShouldCreateConfigWhenNull() { ClusterVO clusterWithNullConfig = ClusterVO.builder() diff --git a/web/src/stores/clusterStore.test.ts b/web/src/stores/clusterStore.test.ts new file mode 100644 index 00000000..3084b22c --- /dev/null +++ b/web/src/stores/clusterStore.test.ts @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { ClusterInfo } from '../api/cluster'; +import { listClusters } from '../services/clusterService'; +import useClusterStore from './clusterStore'; + +vi.mock('../services/clusterService', () => ({ + listClusters: vi.fn(), +})); + +const cluster: ClusterInfo = { + id: 'cluster-prod', + name: 'rocketmq-prod', + nsClusterName: 'ns-prod', + type: 'V5_PROXY_CLUSTER', + endpoint: '10.101.2.1:9876', + status: 'healthy', + version: '5.2.0', + brokers: [], + proxies: [], + nameServers: [], + config: { + flushDiskType: 'SYNC_FLUSH', + autoCreateTopicEnable: false, + autoCreateSubscriptionGroup: false, + maxMessageSize: 4194304, + msgTraceTopicName: 'RMQ_SYS_TRACE_TOPIC4', + fileReservedTime: 72, + writeQueueNums: 16, + readQueueNums: 16, + brokerPermission: 6, + deleteWhen: '04', + }, + topicCount: 256, + groupCount: 128, + tpsHistory: [100, 120], +}; + +describe('clusterStore', () => { + afterEach(() => { + vi.mocked(listClusters).mockReset(); + useClusterStore.setState({ clusters: [], loading: false }); + }); + + it('loads clusters from the cluster service', async () => { + vi.mocked(listClusters).mockResolvedValue([cluster]); + + await useClusterStore.getState().fetchClusters(); + + expect(listClusters).toHaveBeenCalledTimes(1); + expect(useClusterStore.getState()).toMatchObject({ + clusters: [cluster], + loading: false, + }); + }); + + it('resets loading when loading clusters fails', async () => { + const error = new Error('failed to load clusters'); + vi.mocked(listClusters).mockRejectedValue(error); + + await expect(useClusterStore.getState().fetchClusters()).rejects.toThrow(error); + + expect(useClusterStore.getState()).toMatchObject({ + clusters: [], + loading: false, + }); + }); +}); diff --git a/web/src/stores/clusterStore.ts b/web/src/stores/clusterStore.ts index c2770046..48795048 100644 --- a/web/src/stores/clusterStore.ts +++ b/web/src/stores/clusterStore.ts @@ -16,12 +16,11 @@ */ import { create } from 'zustand'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Cluster = any; +import { listClusters } from '../services/clusterService'; +import type { ClusterInfo } from '../api/cluster'; interface ClusterState { - clusters: Cluster[]; + clusters: ClusterInfo[]; loading: boolean; fetchClusters: () => Promise; } @@ -31,8 +30,12 @@ const useClusterStore = create((set) => ({ loading: false, fetchClusters: async () => { set({ loading: true }); - // TODO: call API to fetch clusters - set({ clusters: [], loading: false }); + try { + const clusters = await listClusters(); + set({ clusters }); + } finally { + set({ loading: false }); + } }, }));