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 4f982ccc2 fix(nameserver): enforce canonical names, validate endpoints 
and report concurrent mutations (#2510)
4f982ccc2 is described below

commit 4f982ccc2acb3bfc4c4b561f737058574df01287
Author: btlqql <[email protected]>
AuthorDate: Tue Aug 25 17:49:45 2026 +0800

    fix(nameserver): enforce canonical names, validate endpoints and report 
concurrent mutations (#2510)
    
    * fix(nameserver): enforce canonical registry names
    
    Registry names were not trimmed and rmq_nameserver.name had no unique
    constraint, so 'prod' and 'prod ' (or two concurrent 'prod' creates) could
    both be stored as near-duplicate entries.
    
    Names are now trimmed at the service boundary (blank becomes a 400), the
    reference DDL gains UNIQUE KEY uk_nameserver_name, and unique-index
    violations are mapped to 409 so concurrent creates/rename collisions fail
    with a conflict instead of an opaque 500.
    
    Fixes #2486
    
    * fix(nameserver): validate registry endpoint addresses
    
    namesrvAddr was only checked for non-empty, so empty segments, missing
    ports, out-of-range ports and malformed IPv6 literals could be stored and
    only surfaced later as 'offline' during cluster probing.
    
    Add a NamesrvAddrParser that accepts host:port and [IPv6]:port segments
    separated by commas or semicolons, rejects empty segments and ports outside
    1-65535, and stores the normalized value (trimmed, comma-joined, lowercased
    hosts) on create and update.
    
    Fixes #2487
    
    * fix(nameserver): report concurrent registry mutations accurately
    
    Update ignored the affected-row count of updateById and reloaded the row
    into toVO unguarded, so an entry deleted in between triggered an NPE/500;
    delete likewise ignored the final affected-row count, so two concurrent
    deletes could both report success.
    
    Both paths now check the affected rows and return 404 when the row has
    disappeared instead of failing opaquely or reporting a false success.
    
    Fixes #2488
---
 deploy/mysql/upgrade-nameserver-name-uk.sql        |  35 ++++
 .../nameserver/NameserverRegistryService.java      |  68 ++++++--
 .../cluster/nameserver/NamesrvAddrParser.java      | 102 ++++++++++++
 server/src/main/resources/db/schema.sql            |   3 +-
 .../nameserver/NameserverRegistryServiceTest.java  | 185 +++++++++++++++++++++
 .../cluster/nameserver/NamesrvAddrParserTest.java  | 126 ++++++++++++++
 6 files changed, 506 insertions(+), 13 deletions(-)

diff --git a/deploy/mysql/upgrade-nameserver-name-uk.sql 
b/deploy/mysql/upgrade-nameserver-name-uk.sql
new file mode 100644
index 000000000..5427cdb7f
--- /dev/null
+++ b/deploy/mysql/upgrade-nameserver-name-uk.sql
@@ -0,0 +1,35 @@
+-- deploy/mysql/upgrade-nameserver-name-uk.sql
+-- 存量 MySQL 数据卷增量迁移(2026-08-25):rmq_nameserver.name 全局唯一。
+-- 背景:nameserver 注册名作为业务唯一标识;此前无唯一约束,并发创建可能产生重复记录。
+-- 适用:数据卷已初始化、docker-entrypoint-initdb.d 不会再执行的存量部署。
+--       全新数据卷由 server/src/main/resources/db/schema.sql 直接带上 
uk_nameserver_name。
+-- 幂等:可重复执行。
+--
+-- ⚠️ 会删除同名重复记录(保留 created_at 最早的一条)。执行前可先用下面的查询核对重复:
+--   SELECT name, COUNT(*) FROM rmq_nameserver GROUP BY name HAVING COUNT(*) > 
1;
+--
+-- 用法(远程容器内执行):
+--   docker exec -i rocketmq-studio-mysql mysql -uroot -pstudio123 rocketmq < 
upgrade-nameserver-name-uk.sql
+
+SET NAMES utf8mb4;
+
+-- 1. 清理同名重复(保留 created_at 最早,id 最小作为并列时的决胜)
+DELETE i FROM rmq_nameserver i
+JOIN rmq_nameserver k
+  ON k.name = i.name
+ AND (k.created_at < i.created_at
+      OR (k.created_at = i.created_at AND k.id < i.id));
+
+-- 2. 追加唯一键(仅当不存在时)
+SET @uk_exists := (
+  SELECT COUNT(*) FROM information_schema.statistics
+   WHERE table_schema = DATABASE()
+     AND table_name = 'rmq_nameserver'
+     AND index_name = 'uk_nameserver_name'
+);
+SET @uk_sql := IF(@uk_exists = 0,
+    'ALTER TABLE rmq_nameserver ADD UNIQUE KEY uk_nameserver_name (name)',
+    'SELECT ''uk_nameserver_name already exists'' AS msg');
+PREPARE stmt FROM @uk_sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryService.java
index 2ecbf2db5..2d2c6e750 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryService.java
@@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.persistence.entity.RmqNameserver;
 import org.apache.rocketmq.studio.persistence.mapper.RmqNameserverMapper;
+import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
@@ -38,18 +39,24 @@ public class NameserverRegistryService {
     }
 
     public NameserverRegistryVO create(CreateNameserverRegistryDTO command) {
+        String name = normalizeName(command.getName());
         Long existing = nameserverMapper.selectCount(new 
QueryWrapper<RmqNameserver>()
-                .eq("name", command.getName()));
+                .eq("name", name));
         if (existing != null && existing > 0) {
-            throw new BusinessException(400, "NameServer registry name already 
exists: " + command.getName());
+            throw duplicateName(name);
         }
         RmqNameserver entity = new RmqNameserver();
-        entity.setName(command.getName());
-        entity.setNamesrvAddr(command.getNamesrvAddr());
+        entity.setName(name);
+        
entity.setNamesrvAddr(NamesrvAddrParser.normalize(command.getNamesrvAddr()));
         entity.setK8sNamespace(command.getK8sNamespace());
         entity.setK8sId(command.getK8sId());
         entity.setDescription(command.getDescription());
-        nameserverMapper.insert(entity);
+        try {
+            nameserverMapper.insert(entity);
+        } catch (DataIntegrityViolationException exception) {
+            // The unique index is the final guard against concurrent 
duplicate creates.
+            throw duplicateName(name);
+        }
         return toVO(nameserverMapper.selectById(entity.getId()));
     }
 
@@ -58,26 +65,63 @@ public class NameserverRegistryService {
         if (entity == null) {
             throw new BusinessException(404, "NameServer registry entry not 
found: " + command.getId());
         }
+        String name = normalizeName(command.getName());
         Long duplicates = nameserverMapper.selectCount(new 
QueryWrapper<RmqNameserver>()
-                .eq("name", command.getName())
+                .eq("name", name)
                 .ne("id", command.getId()));
         if (duplicates != null && duplicates > 0) {
-            throw new BusinessException(400, "NameServer registry name already 
exists: " + command.getName());
+            throw duplicateName(name);
         }
-        entity.setName(command.getName());
-        entity.setNamesrvAddr(command.getNamesrvAddr());
+        entity.setName(name);
+        
entity.setNamesrvAddr(NamesrvAddrParser.normalize(command.getNamesrvAddr()));
         entity.setK8sNamespace(command.getK8sNamespace());
         entity.setK8sId(command.getK8sId());
         entity.setDescription(command.getDescription());
-        nameserverMapper.updateById(entity);
-        return toVO(nameserverMapper.selectById(entity.getId()));
+        try {
+            int updated = nameserverMapper.updateById(entity);
+            if (updated == 0) {
+                throw concurrentlyDeleted(command.getId());
+            }
+        } catch (DataIntegrityViolationException exception) {
+            // The unique index is the final guard against concurrent rename 
collisions.
+            throw duplicateName(name);
+        }
+        RmqNameserver stored = nameserverMapper.selectById(entity.getId());
+        if (stored == null) {
+            // The row vanished between the update and the reload; do not 
convert null to a VO.
+            throw concurrentlyDeleted(command.getId());
+        }
+        return toVO(stored);
+    }
+
+    /**
+     * Registry names are compared and displayed as-is, so surrounding 
whitespace must not
+     * create near-duplicate entries ("prod" vs "prod ").
+     */
+    private static String normalizeName(String raw) {
+        String name = raw == null ? "" : raw.trim();
+        if (name.isEmpty()) {
+            throw new BusinessException(400, "name must not be blank");
+        }
+        return name;
+    }
+
+    private static BusinessException duplicateName(String name) {
+        return new BusinessException(409, "NameServer registry name already 
exists: " + name);
     }
 
     public void delete(Long id) {
         if (nameserverMapper.selectById(id) == null) {
             throw new BusinessException(404, "NameServer registry entry not 
found: " + id);
         }
-        nameserverMapper.deleteById(id);
+        if (nameserverMapper.deleteById(id) == 0) {
+            // A concurrent delete already removed the row; report it instead 
of a false success.
+            throw concurrentlyDeleted(id);
+        }
+    }
+
+    private static BusinessException concurrentlyDeleted(Long id) {
+        return new BusinessException(404, "NameServer registry entry was 
deleted concurrently: " + id);
     }
 
     private NameserverRegistryVO toVO(RmqNameserver entity) {
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NamesrvAddrParser.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NamesrvAddrParser.java
new file mode 100644
index 000000000..45bbca3b4
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/nameserver/NamesrvAddrParser.java
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.cluster.nameserver;
+
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parses the NameServer address list stored in the registry. Accepts comma or 
semicolon
+ * separated segments in {@code host:port} or {@code [IPv6]:port} form and 
returns the
+ * normalized value: trimmed segments joined by commas with lowercased hosts.
+ */
+public final class NamesrvAddrParser {
+
+    private static final int MIN_PORT = 1;
+    private static final int MAX_PORT = 65535;
+
+    private NamesrvAddrParser() {
+    }
+
+    public static String normalize(String raw) {
+        if (raw == null || raw.trim().isEmpty()) {
+            throw new BusinessException(400, "namesrvAddr must not be blank");
+        }
+        List<String> segments = new ArrayList<>();
+        for (String part : raw.split("[,;]")) {
+            String segment = part.trim();
+            if (segment.isEmpty()) {
+                throw new BusinessException(400, "namesrvAddr contains an 
empty address segment");
+            }
+            segments.add(normalizeSegment(segment));
+        }
+        return String.join(",", segments);
+    }
+
+    private static String normalizeSegment(String segment) {
+        int portStart = segment.lastIndexOf(':');
+        if (portStart <= 0) {
+            throw new BusinessException(400, "namesrvAddr segment is missing 
host:port: " + segment);
+        }
+        String host = segment.substring(0, portStart);
+        String portText = segment.substring(portStart + 1);
+        String normalizedHost;
+        if (host.startsWith("[") && host.endsWith("]")) {
+            String ipv6 = host.substring(1, host.length() - 1);
+            if (!isValidIpv6Literal(ipv6)) {
+                throw new BusinessException(400, "namesrvAddr segment has a 
malformed IPv6 literal: " + segment);
+            }
+            normalizedHost = "[" + ipv6.toLowerCase() + "]";
+        } else {
+            if (host.isEmpty()) {
+                throw new BusinessException(400, "namesrvAddr segment is 
missing a host: " + segment);
+            }
+            if (host.chars().anyMatch(ch -> ch == ':' || 
Character.isWhitespace(ch))) {
+                throw new BusinessException(400, "namesrvAddr segment has an 
unexpected character: " + segment);
+            }
+            normalizedHost = host.toLowerCase();
+        }
+        int port;
+        try {
+            port = Integer.parseInt(portText);
+        } catch (NumberFormatException exception) {
+            throw new BusinessException(400, "namesrvAddr segment has a 
non-numeric port: " + segment);
+        }
+        if (port < MIN_PORT || port > MAX_PORT) {
+            throw new BusinessException(400, "namesrvAddr port is out of range 
1-65535: " + segment);
+        }
+        return normalizedHost + ":" + port;
+    }
+
+    private static boolean isValidIpv6Literal(String ipv6) {
+        if (ipv6.isEmpty() || ipv6.chars().filter(ch -> ch == ':').count() < 
2) {
+            return false;
+        }
+        for (char ch : ipv6.toCharArray()) {
+            boolean valid = ch == ':'
+                    || Character.isDigit(ch)
+                    || ch >= 'a' && ch <= 'f'
+                    || ch >= 'A' && ch <= 'F';
+            if (!valid) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/server/src/main/resources/db/schema.sql 
b/server/src/main/resources/db/schema.sql
index 9e58a7d78..29a21f063 100644
--- a/server/src/main/resources/db/schema.sql
+++ b/server/src/main/resources/db/schema.sql
@@ -53,7 +53,8 @@ CREATE TABLE IF NOT EXISTS rmq_nameserver (
   k8s_id VARCHAR(128) DEFAULT NULL COMMENT 'k8s ID(K8s 部署场景填写,非 K8s 部署留空)',
   status VARCHAR(32) DEFAULT 'healthy',
   description TEXT,
-  PRIMARY KEY (`id`)
+  PRIMARY KEY (`id`),
+  UNIQUE KEY uk_nameserver_name (name)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- 2. 实例注册表(实例管理页的数据源,topic/group 按 instance_id 归属统计)
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryServiceTest.java
index ee043a8cb..d1bc1e83e 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NameserverRegistryServiceTest.java
@@ -21,9 +21,11 @@ import 
org.apache.rocketmq.studio.persistence.entity.RmqNameserver;
 import org.apache.rocketmq.studio.persistence.mapper.RmqNameserverMapper;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.dao.DuplicateKeyException;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -115,6 +117,89 @@ class NameserverRegistryServiceTest {
         verify(nameserverMapper, never()).insert(any(RmqNameserver.class));
     }
 
+    @Test
+    void createShouldTrimNameBeforeUniquenessCheckAndPersistTest() {
+        when(nameserverMapper.selectCount(any())).thenReturn(0L);
+        
when(nameserverMapper.insert(any(RmqNameserver.class))).thenAnswer(invocation 
-> {
+            RmqNameserver entity = invocation.getArgument(0);
+            entity.setId(10L);
+            return 1;
+        });
+        RmqNameserver stored = new RmqNameserver();
+        stored.setId(10L);
+        stored.setName("prod");
+        when(nameserverMapper.selectById(10L)).thenReturn(stored);
+
+        service.create(CreateNameserverRegistryDTO.builder()
+                .name("  prod  ")
+                .namesrvAddr("10.0.0.1:9876")
+                .build());
+
+        ArgumentCaptor<RmqNameserver> captor = 
ArgumentCaptor.forClass(RmqNameserver.class);
+        verify(nameserverMapper).insert(captor.capture());
+        assertThat(captor.getValue().getName()).isEqualTo("prod");
+    }
+
+    @Test
+    void createShouldRejectBlankNameTest() {
+        assertThatThrownBy(() -> 
service.create(CreateNameserverRegistryDTO.builder()
+                .name("   ")
+                .namesrvAddr("10.0.0.1:9876")
+                .build()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("must not be blank");
+        verify(nameserverMapper, never()).insert(any(RmqNameserver.class));
+    }
+
+    @Test
+    void createShouldNormalizeAddrBeforePersistTest() {
+        when(nameserverMapper.selectCount(any())).thenReturn(0L);
+        
when(nameserverMapper.insert(any(RmqNameserver.class))).thenAnswer(invocation 
-> {
+            RmqNameserver entity = invocation.getArgument(0);
+            entity.setId(11L);
+            return 1;
+        });
+        RmqNameserver stored = new RmqNameserver();
+        stored.setId(11L);
+        stored.setName("prod");
+        stored.setNamesrvAddr("ns1:9876,ns2:9876");
+        when(nameserverMapper.selectById(11L)).thenReturn(stored);
+
+        service.create(CreateNameserverRegistryDTO.builder()
+                .name("prod")
+                .namesrvAddr(" NS1:9876 ; ns2:9876 ")
+                .build());
+
+        ArgumentCaptor<RmqNameserver> captor = 
ArgumentCaptor.forClass(RmqNameserver.class);
+        verify(nameserverMapper).insert(captor.capture());
+        
assertThat(captor.getValue().getNamesrvAddr()).isEqualTo("ns1:9876,ns2:9876");
+    }
+
+    @Test
+    void createShouldRejectMalformedAddrTest() {
+        assertThatThrownBy(() -> 
service.create(CreateNameserverRegistryDTO.builder()
+                .name("prod")
+                .namesrvAddr("ns1")
+                .build()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("missing host:port");
+        verify(nameserverMapper, never()).insert(any(RmqNameserver.class));
+    }
+
+    @Test
+    void createShouldMapUniqueIndexViolationToConflictTest() {
+        when(nameserverMapper.selectCount(any())).thenReturn(0L);
+        when(nameserverMapper.insert(any(RmqNameserver.class)))
+                .thenThrow(new DuplicateKeyException("uk_nameserver_name"));
+
+        assertThatThrownBy(() -> 
service.create(CreateNameserverRegistryDTO.builder()
+                .name("prod")
+                .namesrvAddr("10.0.0.1:9876")
+                .build()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("already exists");
+    }
+
     @Test
     void updateShouldPersistAndReturnStoredEntryTest() {
         RmqNameserver existing = new RmqNameserver();
@@ -122,6 +207,7 @@ class NameserverRegistryServiceTest {
         existing.setName("rocketmq1");
         
when(nameserverMapper.selectById(1L)).thenReturn(existing).thenReturn(existing);
         when(nameserverMapper.selectCount(any())).thenReturn(0L);
+        
when(nameserverMapper.updateById(any(RmqNameserver.class))).thenReturn(1);
 
         NameserverRegistryVO updated = 
service.update(UpdateNameserverRegistryDTO.builder()
                 .id(1L)
@@ -166,17 +252,116 @@ class NameserverRegistryServiceTest {
         verify(nameserverMapper, never()).updateById(any(RmqNameserver.class));
     }
 
+    @Test
+    void updateShouldRejectMalformedAddrTest() {
+        RmqNameserver existing = new RmqNameserver();
+        existing.setId(1L);
+        when(nameserverMapper.selectById(1L)).thenReturn(existing);
+
+        assertThatThrownBy(() -> 
service.update(UpdateNameserverRegistryDTO.builder()
+                .id(1L)
+                .name("rocketmq1")
+                .namesrvAddr("ns1:0")
+                .build()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("out of range");
+        verify(nameserverMapper, never()).updateById(any(RmqNameserver.class));
+    }
+
+    @Test
+    void updateShouldTrimNameBeforePersistTest() {
+        RmqNameserver existing = new RmqNameserver();
+        existing.setId(1L);
+        existing.setName("rocketmq1");
+        
when(nameserverMapper.selectById(1L)).thenReturn(existing).thenReturn(existing);
+        when(nameserverMapper.selectCount(any())).thenReturn(0L);
+        
when(nameserverMapper.updateById(any(RmqNameserver.class))).thenReturn(1);
+
+        service.update(UpdateNameserverRegistryDTO.builder()
+                .id(1L)
+                .name("  rocketmq1  ")
+                .namesrvAddr("rocketmq1-nameserver.svc:9876")
+                .build());
+
+        assertThat(existing.getName()).isEqualTo("rocketmq1");
+    }
+
+    @Test
+    void updateShouldMapUniqueIndexViolationToConflictTest() {
+        RmqNameserver existing = new RmqNameserver();
+        existing.setId(1L);
+        when(nameserverMapper.selectById(1L)).thenReturn(existing);
+        when(nameserverMapper.selectCount(any())).thenReturn(0L);
+        when(nameserverMapper.updateById(any(RmqNameserver.class)))
+                .thenThrow(new DuplicateKeyException("uk_nameserver_name"));
+
+        assertThatThrownBy(() -> 
service.update(UpdateNameserverRegistryDTO.builder()
+                .id(1L)
+                .name("rocketmq2")
+                .namesrvAddr("x:9876")
+                .build()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("already exists");
+    }
+
+    @Test
+    void updateShouldThrowWhenEntryDeletedAfterReadTest() {
+        RmqNameserver existing = new RmqNameserver();
+        existing.setId(1L);
+        when(nameserverMapper.selectById(1L)).thenReturn(existing);
+        when(nameserverMapper.selectCount(any())).thenReturn(0L);
+        
when(nameserverMapper.updateById(any(RmqNameserver.class))).thenReturn(0);
+
+        assertThatThrownBy(() -> 
service.update(UpdateNameserverRegistryDTO.builder()
+                .id(1L)
+                .name("rocketmq1")
+                .namesrvAddr("x:9876")
+                .build()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("deleted concurrently");
+    }
+
+    @Test
+    void updateShouldThrowWhenEntryVanishesBeforeReloadTest() {
+        RmqNameserver existing = new RmqNameserver();
+        existing.setId(1L);
+        
when(nameserverMapper.selectById(1L)).thenReturn(existing).thenReturn(null);
+        when(nameserverMapper.selectCount(any())).thenReturn(0L);
+        
when(nameserverMapper.updateById(any(RmqNameserver.class))).thenReturn(1);
+
+        assertThatThrownBy(() -> 
service.update(UpdateNameserverRegistryDTO.builder()
+                .id(1L)
+                .name("rocketmq1")
+                .namesrvAddr("x:9876")
+                .build()))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("deleted concurrently");
+    }
+
     @Test
     void deleteShouldRemoveExistingEntryTest() {
         RmqNameserver stored = new RmqNameserver();
         stored.setId(1L);
         when(nameserverMapper.selectById(1L)).thenReturn(stored);
+        when(nameserverMapper.deleteById(1L)).thenReturn(1);
 
         service.delete(1L);
 
         verify(nameserverMapper).deleteById(1L);
     }
 
+    @Test
+    void deleteShouldThrowWhenEntryDeletedConcurrentlyTest() {
+        RmqNameserver stored = new RmqNameserver();
+        stored.setId(1L);
+        when(nameserverMapper.selectById(1L)).thenReturn(stored);
+        when(nameserverMapper.deleteById(1L)).thenReturn(0);
+
+        assertThatThrownBy(() -> service.delete(1L))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("deleted concurrently");
+    }
+
     @Test
     void deleteShouldThrowWhenEntryMissingTest() {
         when(nameserverMapper.selectById(404L)).thenReturn(null);
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NamesrvAddrParserTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NamesrvAddrParserTest.java
new file mode 100644
index 000000000..71f1dd6fa
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/nameserver/NamesrvAddrParserTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.cluster.nameserver;
+
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class NamesrvAddrParserTest {
+
+    @Test
+    void keepsWellFormedSingleAddressTest() {
+        
assertThat(NamesrvAddrParser.normalize("10.0.0.1:9876")).isEqualTo("10.0.0.1:9876");
+    }
+
+    @Test
+    void lowercasesHostsTest() {
+        
assertThat(NamesrvAddrParser.normalize("NS1.Example.COM:9876")).isEqualTo("ns1.example.com:9876");
+    }
+
+    @Test
+    void normalizesSeparatorsAndWhitespaceTest() {
+        assertThat(NamesrvAddrParser.normalize(" ns1:9876 ; ns2:9876 ,ns3:9876 
"))
+                .isEqualTo("ns1:9876,ns2:9876,ns3:9876");
+    }
+
+    @Test
+    void supportsIpv6LiteralsTest() {
+        
assertThat(NamesrvAddrParser.normalize("[2001:DB8::1]:9876")).isEqualTo("[2001:db8::1]:9876");
+    }
+
+    @Test
+    void rejectsBlankInputTest() {
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("   "))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("must not be blank");
+    }
+
+    @Test
+    void rejectsEmptySegmentsTest() {
+        assertThatThrownBy(() -> 
NamesrvAddrParser.normalize("ns1:9876,,ns2:9876"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("empty address segment");
+    }
+
+    @Test
+    void rejectsConsecutiveSeparatorsTest() {
+        assertThatThrownBy(() -> 
NamesrvAddrParser.normalize("ns1:9876;;ns2:9876"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("empty address segment");
+    }
+
+    @Test
+    void rejectsMissingPortTest() {
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("ns1"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("missing host:port");
+    }
+
+    @Test
+    void rejectsMissingHostTest() {
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize(":9876"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("missing host:port");
+    }
+
+    @Test
+    void rejectsNonNumericPortTest() {
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("ns1:abc"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("non-numeric port");
+    }
+
+    @Test
+    void rejectsPortsOutOfRangeTest() {
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("ns1:0"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("out of range");
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("ns1:65536"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("out of range");
+    }
+
+    @Test
+    void rejectsUnexpectedColonInHostTest() {
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("a:b:9876"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("unexpected character");
+    }
+
+    @Test
+    void rejectsWhitespaceInsideHostTest() {
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("ns 1:9876"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("unexpected character");
+    }
+
+    @Test
+    void rejectsMalformedIpv6LiteralsTest() {
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("[abc]:9876"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("malformed IPv6");
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("[1:2]:9876"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("malformed IPv6");
+        assertThatThrownBy(() -> NamesrvAddrParser.normalize("[::1-g]:9876"))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("malformed IPv6");
+    }
+}

Reply via email to