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

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 7b0eaea89c test(admin): add coverage for service implementations 
(#7548)
7b0eaea89c is described below

commit 7b0eaea89c1dd17d2161f2f2d9c89a7c8cadb613
Author: Southern <[email protected]>
AuthorDate: Mon Oct 12 09:11:41 2026 +0800

    test(admin): add coverage for service implementations (#7548)
---
 .../service/AiProxyConnectionServiceImplTest.java  | 110 ++++++++
 .../admin/service/DiscoveryServiceImplTest.java    | 294 ++++++++++++++++++++-
 .../service/NamespaceUserServiceImplTest.java      |  71 +++++
 .../service/OperationRecordLogServiceImplTest.java | 111 ++++++++
 .../admin/service/RegistryServiceImplTest.java     | 145 ++++++++++
 .../admin/service/ScalePolicyServiceImplTest.java  | 105 ++++++++
 .../shenyu/admin/service/ScaleRuleServiceTest.java | 113 +++++++-
 .../manager/RegisterApiDocServiceImplTest.java     | 162 ++++++++++++
 .../ShenyuClientRegisterMcpServiceImplTest.java    | 172 ++++++++++++
 .../plugin/base/alert/AlarmServiceImplTest.java    | 108 ++++++++
 10 files changed, 1380 insertions(+), 11 deletions(-)

diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/AiProxyConnectionServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/AiProxyConnectionServiceImplTest.java
new file mode 100644
index 0000000000..ce26988396
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/AiProxyConnectionServiceImplTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.shenyu.admin.service;
+
+import org.apache.shenyu.admin.listener.DataChangedEvent;
+import org.apache.shenyu.admin.mapper.AiProxyApiKeyMapper;
+import org.apache.shenyu.admin.model.entity.ProxyApiKeyDO;
+import org.apache.shenyu.admin.service.impl.AiProxyConnectionServiceImpl;
+import org.apache.shenyu.admin.service.support.AiProxyRealKeyResolver;
+import org.apache.shenyu.common.dto.ProxyApiKeyData;
+import org.apache.shenyu.common.enums.ConfigGroupEnum;
+import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mock;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link AiProxyConnectionServiceImpl}. */
+@ExtendWith(MockitoExtension.class)
+public final class AiProxyConnectionServiceImplTest {
+
+    @Mock
+    private AiProxyRealKeyResolver resolver;
+
+    @Mock
+    private AiProxyApiKeyMapper mapper;
+
+    @Mock
+    private org.springframework.context.ApplicationEventPublisher publisher;
+
+    @InjectMocks
+    private AiProxyConnectionServiceImpl service;
+
+    @Test
+    public void testRefreshWithNoKeysOnlyInvalidates() {
+        
when(mapper.selectBySelectorId("s1")).thenReturn(Collections.emptyList());
+        service.refreshApiKeysBySelectorId("s1");
+        verify(resolver).invalidate("s1");
+        verifyNoInteractions(publisher);
+    }
+
+    @Test
+    public void testRefreshPublishesResolvedData() {
+        ProxyApiKeyDO key = new ProxyApiKeyDO();
+        key.setSelectorId("s1");
+        key.setNamespaceId(null);
+        key.setProxyApiKey("proxy");
+        key.setEnabled(Boolean.TRUE);
+        key.setDescription("desc");
+        
when(mapper.selectBySelectorId("s1")).thenReturn(Collections.singletonList(key));
+        when(resolver.resolveRealKey("s1")).thenReturn(Optional.of("real"));
+        service.refreshApiKeysBySelectorId("s1");
+        ArgumentCaptor<DataChangedEvent> captor = 
ArgumentCaptor.forClass(DataChangedEvent.class);
+        verify(publisher).publishEvent(captor.capture());
+        ProxyApiKeyData data = (ProxyApiKeyData) ((java.util.List<?>) 
captor.getValue().getSource()).get(0);
+        assertEquals("real", data.getRealApiKey());
+        assertEquals("proxy", data.getProxyApiKey());
+        assertEquals(Boolean.TRUE, data.getEnabled());
+        assertEquals("s1", data.getSelectorId());
+        assertEquals("desc", data.getDescription());
+        
assertEquals(org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_ID,
 data.getNamespaceId());
+        assertEquals(ConfigGroupEnum.AI_PROXY_API_KEY, 
captor.getValue().getGroupKey());
+        assertEquals(DataEventTypeEnum.UPDATE, 
ReflectionTestUtils.getField(captor.getValue(), "eventType"));
+    }
+
+    @Test
+    public void testRefreshHandlesMissingRealKeyAndDisabledMapping() {
+        ProxyApiKeyDO key = new ProxyApiKeyDO();
+        key.setSelectorId("s1");
+        key.setNamespaceId("custom");
+        key.setProxyApiKey("proxy");
+        key.setEnabled(Boolean.FALSE);
+        
when(mapper.selectBySelectorId("s1")).thenReturn(Collections.singletonList(key));
+        when(resolver.resolveRealKey("s1")).thenReturn(Optional.empty());
+        service.refreshApiKeysBySelectorId("s1");
+        ArgumentCaptor<DataChangedEvent> captor = 
ArgumentCaptor.forClass(DataChangedEvent.class);
+        verify(publisher).publishEvent(captor.capture());
+        ProxyApiKeyData data = (ProxyApiKeyData) ((java.util.List<?>) 
captor.getValue().getSource()).get(0);
+        assertNull(data.getRealApiKey());
+        assertEquals(Boolean.FALSE, data.getEnabled());
+        assertEquals("custom", data.getNamespaceId());
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryServiceImplTest.java
index b640bcf47a..b803bef72e 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryServiceImplTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/DiscoveryServiceImplTest.java
@@ -27,39 +27,50 @@ import org.apache.shenyu.admin.mapper.SelectorMapper;
 import org.apache.shenyu.admin.model.dto.DiscoveryDTO;
 import org.apache.shenyu.admin.model.dto.DiscoveryHandlerDTO;
 import org.apache.shenyu.admin.model.dto.DiscoveryRelDTO;
+import org.apache.shenyu.admin.model.dto.ProxySelectorDTO;
 import org.apache.shenyu.admin.model.entity.DiscoveryDO;
 import org.apache.shenyu.admin.model.entity.DiscoveryRelDO;
 import org.apache.shenyu.admin.service.configs.ConfigsImportContext;
+import org.apache.shenyu.admin.model.entity.DiscoveryHandlerDO;
+import org.apache.shenyu.admin.model.entity.ProxySelectorDO;
 import org.apache.shenyu.admin.model.entity.SelectorDO;
-import org.apache.shenyu.register.common.dto.DiscoveryConfigRegisterDTO;
-import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.transaction.support.SimpleTransactionStatus;
-import org.springframework.transaction.support.TransactionSynchronization;
-import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.apache.shenyu.admin.model.result.ConfigImportResult;
+import org.apache.shenyu.admin.model.vo.DiscoveryVO;
 import org.apache.shenyu.admin.service.impl.DiscoveryServiceImpl;
 import org.apache.shenyu.admin.utils.ShenyuResultMessage;
 import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.register.common.dto.DiscoveryConfigRegisterDTO;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.Mock;
 import org.mockito.quality.Strictness;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.support.SimpleTransactionStatus;
+import org.springframework.transaction.support.TransactionSynchronization;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import java.util.Collections;
+import java.util.List;
 
 import static 
org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_ID;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
-import org.mockito.ArgumentCaptor;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.when;
 
 /**
  * Test cases for DiscoveryServiceImpl.
@@ -139,6 +150,37 @@ public final class DiscoveryServiceImplTest {
         verify(discoveryProcessor).createProxySelector(any(), any());
     }
 
+    @Test
+    public void testBindingRetriesAndFailsWhenSelectorIsMissing() {
+        DiscoveryConfigRegisterDTO config = bindingConfig();
+        
given(selectorService.findByNameAndPluginNameAndNamespaceIdForUpdate("test", 
"divide", SYS_DEFAULT_NAMESPACE_ID))
+                .willReturn(null);
+        assertThrows(ShenyuException.class, () -> 
discoveryService.registerDiscoveryConfig(config));
+        verify(selectorService, org.mockito.Mockito.times(3))
+                .findByNameAndPluginNameAndNamespaceIdForUpdate("test", 
"divide", SYS_DEFAULT_NAMESPACE_ID);
+        verifyNoInteractions(discoveryMapper, discoveryHandlerMapper, 
discoveryRelMapper, discoveryProcessor);
+    }
+
+    @Test
+    public void testBindingReusesExistingDiscoveryAndHandler() {
+        SelectorDO selector = new SelectorDO();
+        selector.setId("selector");
+        selector.setSelectorName("test");
+        selector.setNamespaceId(SYS_DEFAULT_NAMESPACE_ID);
+        
given(selectorService.findByNameAndPluginNameAndNamespaceIdForUpdate("test", 
"divide", SYS_DEFAULT_NAMESPACE_ID))
+                .willReturn(selector);
+        DiscoveryDO existing = 
DiscoveryDO.builder().id("discovery").discoveryType("local").build();
+        DiscoveryHandlerDO handler = 
DiscoveryHandlerDO.builder().id("handler").discoveryId("discovery").build();
+        
given(discoveryMapper.selectByPluginNameAndLevelAndNamespaceIdAndType(anyString(),
 anyString(), anyString(), anyString()))
+                .willReturn(existing);
+        
given(discoveryHandlerMapper.selectBySelectorId("selector")).willReturn(handler);
+        discoveryService.registerDiscoveryConfig(bindingConfig());
+        verify(discoveryMapper, never()).insertSelective(any());
+        verify(discoveryHandlerMapper, never()).insertSelective(any());
+        verify(discoveryRelMapper, never()).insertSelective(any());
+        verify(discoveryProcessor).createDiscovery(existing);
+    }
+
     private DiscoveryConfigRegisterDTO bindingConfig() {
         SelectorDO selector = new SelectorDO();
         selector.setId("selector");
@@ -278,4 +320,238 @@ public final class DiscoveryServiceImplTest {
         assertEquals("", relCaptor.getValue().getSelectorId());
         assertEquals("proxy-selector-new", 
relCaptor.getValue().getProxySelectorId());
     }
+
+    @Test
+    public void testDeleteWithHandlerBound() {
+        given(discoveryHandlerMapper.selectByDiscoveryId("discovery-1"))
+                
.willReturn(Collections.singletonList(DiscoveryHandlerDO.builder().id("handler").build()));
+        assertThrows(ShenyuException.class, () -> 
discoveryService.delete("discovery-1", SYS_DEFAULT_NAMESPACE_ID));
+        verify(discoveryMapper, never()).selectById("discovery-1");
+    }
+
+    @Test
+    public void testTypeCreateUpdateAndDiscoveryLookup() {
+        
assertEquals(org.apache.shenyu.admin.model.enums.DiscoveryTypeEnum.types(), 
discoveryService.typeEnums());
+        DiscoveryDTO dto = new DiscoveryDTO();
+        dto.setName("d");
+        dto.setPluginName("divide");
+        dto.setType("local");
+        dto.setLevel("plugin");
+        dto.setNamespaceId(SYS_DEFAULT_NAMESPACE_ID);
+        when(discoveryMapper.insert(any())).thenReturn(1);
+        assertEquals("local", 
discoveryService.createOrUpdate(dto).getDiscoveryType());
+        dto.setId("id");
+        when(discoveryMapper.updateSelective(any())).thenReturn(1);
+        assertEquals("local", 
discoveryService.createOrUpdate(dto).getDiscoveryType());
+        dto.setId(null);
+        when(discoveryMapper.insert(any())).thenReturn(0);
+        assertNull(discoveryService.createOrUpdate(dto));
+        discoveryService.discovery("divide", "plugin", 
SYS_DEFAULT_NAMESPACE_ID);
+        
verify(discoveryMapper).selectByPluginNameAndLevelAndNamespaceId("divide", 
"plugin", SYS_DEFAULT_NAMESPACE_ID);
+    }
+
+    @Test
+    public void testSyncAndListData() {
+        SelectorDO selector = new SelectorDO();
+        selector.setId("s");
+        selector.setSelectorName("selector");
+        selector.setNamespaceId(SYS_DEFAULT_NAMESPACE_ID);
+        
when(proxySelectorMapper.selectByDiscoveryId("d")).thenReturn(Collections.emptyList());
+        
when(selectorMapper.selectByDiscoveryId("d")).thenReturn(Collections.singletonList(selector));
+        
when(discoveryHandlerMapper.selectBySelectorId("s")).thenReturn(DiscoveryHandlerDO.builder().id("h").discoveryId("d").build());
+        DiscoveryDO discovery = 
DiscoveryDO.builder().id("d").pluginName("divide").discoveryType("local").namespaceId(SYS_DEFAULT_NAMESPACE_ID).build();
+        discoveryService.syncData(Collections.singletonList(discovery));
+        verify(discoveryProcessor).createDiscovery(discovery);
+        verify(discoveryProcessor).fetchAll(any(), any());
+        
when(discoveryMapper.selectAll()).thenReturn(Collections.singletonList(discovery));
+        
when(discoveryHandlerMapper.selectAll()).thenReturn(Collections.emptyList());
+        
when(discoveryRelMapper.selectAll()).thenReturn(Collections.emptyList());
+        assertEquals(1, discoveryService.listAllData().size());
+        
when(discoveryMapper.selectAllByNamespaceId(SYS_DEFAULT_NAMESPACE_ID)).thenReturn(Collections.emptyList());
+        assertEquals(0, 
discoveryService.listAllDataByNamespaceId(SYS_DEFAULT_NAMESPACE_ID).size());
+        discoveryService.syncData();
+        discoveryService.syncDataByNamespaceId(SYS_DEFAULT_NAMESPACE_ID);
+    }
+
+    @Test
+    public void testSyncDataWithProxySelector() {
+        final DiscoveryDO discovery = 
DiscoveryDO.builder().id("d").pluginName("divide").discoveryType("local").build();
+        ProxySelectorDO proxySelector = 
ProxySelectorDO.builder().id("proxy").name("proxy-name")
+                
.pluginName("divide").namespaceId(SYS_DEFAULT_NAMESPACE_ID).build();
+        DiscoveryHandlerDO handler = 
DiscoveryHandlerDO.builder().id("handler").discoveryId("d").build();
+        
when(proxySelectorMapper.selectByDiscoveryId("d")).thenReturn(Collections.singletonList(proxySelector));
+        
when(discoveryHandlerMapper.selectByProxySelectorId("proxy")).thenReturn(handler);
+        
when(selectorMapper.selectByDiscoveryId("d")).thenReturn(Collections.emptyList());
+        discoveryService.syncData(Collections.singletonList(discovery));
+        ArgumentCaptor<DiscoveryHandlerDTO> handlerCaptor = 
ArgumentCaptor.forClass(DiscoveryHandlerDTO.class);
+        ArgumentCaptor<ProxySelectorDTO> selectorCaptor = 
ArgumentCaptor.forClass(ProxySelectorDTO.class);
+        
verify(discoveryProcessor).createProxySelector(handlerCaptor.capture(), 
selectorCaptor.capture());
+        assertEquals("handler", handlerCaptor.getValue().getId());
+        assertEquals("proxy", selectorCaptor.getValue().getId());
+        assertEquals("proxy-name", selectorCaptor.getValue().getName());
+        assertEquals(SYS_DEFAULT_NAMESPACE_ID, 
selectorCaptor.getValue().getNamespaceId());
+        verify(discoveryProcessor).fetchAll(argThat(dto -> 
"handler".equals(dto.getId())), eq(selectorCaptor.getValue()));
+    }
+
+    @Test
+    public void testListDataWithRelationsAndFindHandler() {
+        DiscoveryDO discovery = 
DiscoveryDO.builder().id("d").discoveryName("name").discoveryType("local").build();
+        DiscoveryHandlerDO handler = 
DiscoveryHandlerDO.builder().id("h").discoveryId("d").build();
+        DiscoveryRelDO relation = 
DiscoveryRelDO.builder().id("r").discoveryHandlerId("h").pluginName("divide").build();
+        
when(discoveryMapper.selectAll()).thenReturn(Collections.singletonList(discovery));
+        
when(discoveryHandlerMapper.selectAll()).thenReturn(Collections.singletonList(handler));
+        
when(discoveryRelMapper.selectAll()).thenReturn(Collections.singletonList(relation));
+        List<DiscoveryVO> all = discoveryService.listAllData();
+        assertEquals("h", all.get(0).getDiscoveryHandler().getId());
+        assertEquals("r", all.get(0).getDiscoveryRel().getId());
+        
when(discoveryHandlerMapper.selectBySelectorId("selector")).thenReturn(handler);
+        assertEquals("h", 
discoveryService.findDiscoveryHandlerBySelectorId("selector").getId());
+    }
+
+    @Test
+    public void testListDataByNamespaceWithHandlerAndRelation() {
+        DiscoveryDO discovery = 
DiscoveryDO.builder().id("d").discoveryType("local").build();
+        DiscoveryHandlerDO handler = 
DiscoveryHandlerDO.builder().id("h").discoveryId("d").build();
+        DiscoveryRelDO relation = 
DiscoveryRelDO.builder().id("r").discoveryHandlerId("h").build();
+        
when(discoveryMapper.selectAllByNamespaceId("ns")).thenReturn(Collections.singletonList(discovery));
+        
when(discoveryHandlerMapper.selectByDiscoveryIds(any())).thenReturn(Collections.singletonList(handler));
+        
when(discoveryRelMapper.selectAll()).thenReturn(Collections.singletonList(relation));
+        assertEquals("r", 
discoveryService.listAllDataByNamespaceId("ns").get(0).getDiscoveryRel().getId());
+    }
+
+    @Test
+    public void testRegisterDefaultAndImport() {
+        when(discoveryHandlerMapper.selectBySelectorId("s")).thenReturn(null);
+        String handlerId = discoveryService.registerDefaultDiscovery("s", 
"divide", SYS_DEFAULT_NAMESPACE_ID);
+        org.junit.jupiter.api.Assertions.assertNotNull(handlerId);
+        verify(discoveryMapper).insertSelective(any());
+        
when(discoveryHandlerMapper.selectBySelectorId("existing")).thenReturn(DiscoveryHandlerDO.builder().id("h").build());
+        assertEquals("h", 
discoveryService.registerDefaultDiscovery("existing", "divide", 
SYS_DEFAULT_NAMESPACE_ID));
+        assertEquals(0, 
discoveryService.importData(Collections.emptyList()).getSuccessCount());
+        DiscoveryDTO dto = new DiscoveryDTO();
+        dto.setName("new");
+        dto.setPluginName("divide");
+        dto.setType("local");
+        dto.setLevel("plugin");
+        when(discoveryMapper.selectAll()).thenReturn(Collections.emptyList());
+        when(discoveryMapper.insert(any())).thenReturn(1);
+        ConfigImportResult result = 
discoveryService.importData(Collections.singletonList(dto));
+        assertEquals(1, result.getSuccessCount());
+        DiscoveryDO existing = 
DiscoveryDO.builder().pluginName("divide").discoveryName("duplicate").build();
+        
when(discoveryMapper.selectAll()).thenReturn(Collections.singletonList(existing));
+        DiscoveryDTO duplicate = new DiscoveryDTO();
+        duplicate.setPluginName("divide");
+        duplicate.setName("duplicate");
+        assertEquals(0, 
discoveryService.importData(Collections.singletonList(duplicate)).getSuccessCount());
+        ConfigsImportContext context = new ConfigsImportContext();
+        
when(discoveryMapper.selectAllByNamespaceId("ns")).thenReturn(Collections.emptyList());
+        assertEquals(1, discoveryService.importData("ns", 
Collections.singletonList(dto), context).getSuccessCount());
+    }
+
+    @Test
+    public void testImportDataWithHandlerAndRelation() {
+        DiscoveryDTO dto = new DiscoveryDTO();
+        dto.setName("new");
+        dto.setPluginName("divide");
+        dto.setType("local");
+        DiscoveryHandlerDTO handler = new DiscoveryHandlerDTO();
+        handler.setId("handler");
+        handler.setHandler("{}");
+        dto.setDiscoveryHandler(handler);
+        DiscoveryRelDTO relation = new DiscoveryRelDTO();
+        relation.setId("relation");
+        relation.setDiscoveryHandlerId("handler");
+        relation.setSelectorId("selector");
+        dto.setDiscoveryRel(relation);
+        when(discoveryMapper.selectAll()).thenReturn(Collections.emptyList());
+        when(discoveryMapper.insert(any())).thenReturn(1);
+        assertEquals(1, 
discoveryService.importData(Collections.singletonList(dto)).getSuccessCount());
+        ArgumentCaptor<DiscoveryHandlerDO> handlerCaptor = 
ArgumentCaptor.forClass(DiscoveryHandlerDO.class);
+        ArgumentCaptor<DiscoveryRelDO> relationCaptor = 
ArgumentCaptor.forClass(DiscoveryRelDO.class);
+        
verify(discoveryHandlerMapper).insertSelective(handlerCaptor.capture());
+        verify(discoveryRelMapper).insertSelective(relationCaptor.capture());
+        assertEquals(dto.getId(), handlerCaptor.getValue().getDiscoveryId());
+        assertEquals("handler", handlerCaptor.getValue().getId());
+        assertEquals("{}", handlerCaptor.getValue().getHandler());
+        assertEquals("relation", relationCaptor.getValue().getId());
+        assertEquals("handler", 
relationCaptor.getValue().getDiscoveryHandlerId());
+        assertEquals("selector", relationCaptor.getValue().getSelectorId());
+    }
+
+    @Test
+    public void testNamespaceImportMapsHandlerAndSelectorIds() {
+        DiscoveryDTO dto = new DiscoveryDTO();
+        dto.setName("new");
+        dto.setPluginName("divide");
+        dto.setType("local");
+        DiscoveryHandlerDTO handler = new DiscoveryHandlerDTO();
+        handler.setId("old-handler");
+        dto.setDiscoveryHandler(handler);
+        DiscoveryRelDTO relation = new DiscoveryRelDTO();
+        relation.setSelectorId("old-selector");
+        relation.setProxySelectorId("old-proxy");
+        dto.setDiscoveryRel(relation);
+        ConfigsImportContext context = new ConfigsImportContext();
+        context.getSelectorIdMapping().put("old-selector", "new-selector");
+        context.getProxySelectorIdMapping().put("old-proxy", "new-proxy");
+        
when(discoveryMapper.selectAllByNamespaceId("ns")).thenReturn(Collections.emptyList());
+        when(discoveryMapper.insert(any())).thenReturn(1);
+        assertEquals(1, discoveryService.importData("ns", 
Collections.singletonList(dto), context).getSuccessCount());
+        ArgumentCaptor<DiscoveryHandlerDO> handlerCaptor = 
ArgumentCaptor.forClass(DiscoveryHandlerDO.class);
+        ArgumentCaptor<DiscoveryRelDO> relationCaptor = 
ArgumentCaptor.forClass(DiscoveryRelDO.class);
+        
verify(discoveryHandlerMapper).insertSelective(handlerCaptor.capture());
+        verify(discoveryRelMapper).insertSelective(relationCaptor.capture());
+        String newHandlerId = 
context.getDiscoveryHandlerIdMapping().get("old-handler");
+        assertNotNull(newHandlerId);
+        assertEquals(newHandlerId, handlerCaptor.getValue().getId());
+        assertEquals(dto.getId(), handlerCaptor.getValue().getDiscoveryId());
+        assertEquals(newHandlerId, 
relationCaptor.getValue().getDiscoveryHandlerId());
+        assertEquals("new-selector", 
relationCaptor.getValue().getSelectorId());
+        assertEquals("new-proxy", 
relationCaptor.getValue().getProxySelectorId());
+        assertEquals("ns", dto.getNamespaceId());
+    }
+
+    @Test
+    public void testUpdateReturnsNullWhenNoRecordIsUpdated() {
+        DiscoveryDTO dto = new DiscoveryDTO();
+        dto.setId("missing");
+        when(discoveryMapper.updateSelective(any())).thenReturn(0);
+        assertNull(discoveryService.createOrUpdate(dto));
+        verifyNoInteractions(discoveryProcessor);
+    }
+
+    @Test
+    public void testFindDiscoveryHandlerWhenSelectorIsNotBound() {
+        
assertNull(discoveryService.findDiscoveryHandlerBySelectorId("unbound"));
+        verify(discoveryHandlerMapper).selectBySelectorId("unbound");
+    }
+
+    @Test
+    public void testDiscoveryLookupMapsRecord() {
+        DiscoveryDO discovery = 
DiscoveryDO.builder().id("d").discoveryType("local")
+                
.pluginName("divide").namespaceId("ns").discoveryLevel("0").serverList("localhost:8848").props("{}").build();
+        
when(discoveryMapper.selectByPluginNameAndLevelAndNamespaceId("divide", "0", 
"ns")).thenReturn(discovery);
+        DiscoveryVO result = discoveryService.discovery("divide", "0", "ns");
+        assertEquals("d", result.getId());
+        assertEquals("local", result.getDiscoveryType());
+        assertEquals("divide", result.getPluginName());
+        assertEquals("ns", result.getNamespaceId());
+        assertEquals("0", result.getLevel());
+        assertEquals("localhost:8848", result.getServerList());
+        assertEquals("{}", result.getProps());
+    }
+
+    @Test
+    public void testNamespaceImportRejectsDuplicateName() {
+        DiscoveryDTO dto = new DiscoveryDTO();
+        dto.setName("duplicate");
+        dto.setPluginName("divide");
+        
when(discoveryMapper.selectAllByNamespaceId("ns")).thenReturn(Collections.singletonList(
+                
DiscoveryDO.builder().pluginName("divide").discoveryName("duplicate").build()));
+        ConfigImportResult result = discoveryService.importData("ns", 
Collections.singletonList(dto), new ConfigsImportContext());
+        assertEquals(0, result.getSuccessCount());
+        assertEquals("import fail discovery: duplicate", 
result.getFailMessage());
+        verify(discoveryMapper, never()).insert(any());
+        verifyNoInteractions(discoveryHandlerMapper, discoveryRelMapper, 
discoveryProcessor);
+    }
 }
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/NamespaceUserServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/NamespaceUserServiceImplTest.java
new file mode 100644
index 0000000000..b9b0c2ccdb
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/NamespaceUserServiceImplTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.shenyu.admin.service;
+
+import org.apache.shenyu.admin.exception.ShenyuAdminException;
+import org.apache.shenyu.admin.mapper.NamespaceUserRelMapper;
+import org.apache.shenyu.admin.model.entity.NamespaceDO;
+import org.apache.shenyu.admin.model.entity.NamespaceUserRelDO;
+import org.apache.shenyu.admin.model.event.namespace.NamespaceCreatedEvent;
+import org.apache.shenyu.admin.service.impl.NamespaceUserServiceImpl;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link NamespaceUserServiceImpl}. */
+@ExtendWith(MockitoExtension.class)
+public final class NamespaceUserServiceImplTest {
+    @Mock
+    private NamespaceUserRelMapper mapper;
+
+    @InjectMocks
+    private NamespaceUserServiceImpl service;
+
+    @Test
+    public void testCreateAndList() {
+        when(mapper.selectByNamespaceIdAndUserId("n", "u")).thenReturn(null);
+        assertEquals("n", service.create("n", "u").getNamespaceId());
+        
when(mapper.selectListByUserId("u")).thenReturn(Arrays.asList(NamespaceUserRelDO.builder().namespaceId("n1").build(),
+                NamespaceUserRelDO.builder().namespaceId("n2").build()));
+        assertEquals(Arrays.asList("n1", "n2"), 
service.listNamespaceIdByUserId("u"));
+        when(mapper.selectListByUserId("empty")).thenReturn(null);
+        assertEquals(Collections.emptyList(), 
service.listNamespaceIdByUserId("empty"));
+        verify(mapper).insertSelective(any(NamespaceUserRelDO.class));
+    }
+
+    @Test
+    public void testDuplicateAndNamespaceEvent() {
+        when(mapper.selectByNamespaceIdAndUserId("n", 
"u")).thenReturn(NamespaceUserRelDO.builder().build());
+        assertThrows(ShenyuAdminException.class, () -> service.create("n", 
"u"));
+        NamespaceDO namespace = 
NamespaceDO.builder().namespaceId("n2").build();
+        when(mapper.selectByNamespaceIdAndUserId("n2", "u2")).thenReturn(null);
+        service.onNamespaceCreated(new NamespaceCreatedEvent(namespace, "u2"));
+        verify(mapper).selectByNamespaceIdAndUserId("n2", "u2");
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/OperationRecordLogServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/OperationRecordLogServiceImplTest.java
new file mode 100644
index 0000000000..d19c506016
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/OperationRecordLogServiceImplTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.shenyu.admin.service;
+
+import org.apache.shenyu.admin.config.properties.DashboardProperties;
+import org.apache.shenyu.admin.exception.ValidFailException;
+import org.apache.shenyu.admin.mapper.OperationRecordLogMapper;
+import org.apache.shenyu.admin.model.custom.UserInfo;
+import org.apache.shenyu.admin.model.query.RecordLogQueryCondition;
+import org.apache.shenyu.admin.service.impl.OperationRecordLogServiceImpl;
+import org.apache.shenyu.admin.utils.SessionUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mock;
+
+import java.util.Collections;
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link OperationRecordLogServiceImpl}. */
+@ExtendWith(MockitoExtension.class)
+public final class OperationRecordLogServiceImplTest {
+    @Mock
+    private OperationRecordLogMapper mapper;
+
+    @Mock
+    private DashboardProperties properties;
+
+    @InjectMocks
+    private OperationRecordLogServiceImpl service;
+
+    @AfterEach
+    public void cleanSession() {
+        SessionUtil.clean();
+    }
+
+    @Test
+    public void testConditionSearchAndList() {
+        
SessionUtil.setLocalVisitor(UserInfo.builder().userName("operator").build());
+        RecordLogQueryCondition condition = new RecordLogQueryCondition();
+        condition.setStartTime(new Date(1_000));
+        condition.setEndTime(new Date(2_000));
+        service.doConditionPreProcessing(condition);
+        assertEquals("operator", condition.getUsername());
+        
when(mapper.selectByCondition(condition)).thenReturn(Collections.emptyList());
+        assertTrue(service.searchByCondition(condition).isEmpty());
+        when(properties.getRecordLogLimit()).thenReturn(10);
+        when(mapper.selectLimit("operator", 
10)).thenReturn(Collections.emptyList());
+        assertTrue(service.list().isEmpty());
+        verify(mapper).selectByCondition(condition);
+        verify(mapper).selectLimit("operator", 10);
+    }
+
+    @Test
+    public void testAdminConditionPreservesUsernameAndListsAllUsers() {
+        
SessionUtil.setLocalVisitor(UserInfo.builder().userName(org.apache.shenyu.common.constant.AdminConstants.ADMIN_NAME).build());
+        RecordLogQueryCondition adminCondition = new RecordLogQueryCondition();
+        adminCondition.setStartTime(new Date(1_000));
+        adminCondition.setEndTime(new Date(2_000));
+        adminCondition.setUsername("requested-user");
+        service.doConditionPreProcessing(adminCondition);
+        assertEquals("requested-user", adminCondition.getUsername());
+        when(properties.getRecordLogLimit()).thenReturn(10);
+        service.list();
+        verify(mapper).selectLimit(null, 10);
+    }
+
+    @Test
+    public void testInvalidConditionAndCleanHistory() {
+        RecordLogQueryCondition condition = new RecordLogQueryCondition();
+        condition.setStartTime(new Date(2_000));
+        condition.setEndTime(new Date(1_000));
+        assertThrows(ValidFailException.class, () -> 
service.doConditionPreProcessing(condition));
+        when(properties.getOnlyCleanDays()).thenReturn(1);
+        assertThrows(ValidFailException.class, () -> service.cleanHistory(new 
Date()));
+        when(mapper.deleteByBefore(any(Date.class))).thenReturn(1);
+        assertTrue(service.cleanHistory(new Date(System.currentTimeMillis() - 
3L * 24 * 60 * 60 * 1000)));
+    }
+
+    @Test
+    public void testCleanHistoryReturnsFalseWhenNothingIsDeleted() {
+        when(properties.getOnlyCleanDays()).thenReturn(1);
+        when(mapper.deleteByBefore(any(Date.class))).thenReturn(0);
+        assertFalse(service.cleanHistory(new Date(System.currentTimeMillis() - 
3L * 24 * 60 * 60 * 1000)));
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RegistryServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RegistryServiceImplTest.java
new file mode 100644
index 0000000000..be858eec6b
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/RegistryServiceImplTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.shenyu.admin.service;
+
+import org.apache.shenyu.admin.exception.ShenyuAdminException;
+import org.apache.shenyu.admin.mapper.RegistryMapper;
+import org.apache.shenyu.admin.model.dto.RegistryDTO;
+import org.apache.shenyu.admin.model.entity.RegistryDO;
+import org.apache.shenyu.admin.model.vo.RegistryVO;
+import org.apache.shenyu.admin.service.impl.RegistryServiceImpl;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mock;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link RegistryServiceImpl}. */
+@ExtendWith(MockitoExtension.class)
+public final class RegistryServiceImplTest {
+
+    @Mock
+    private RegistryMapper registryMapper;
+
+    @InjectMocks
+    private RegistryServiceImpl registryService;
+
+    @Test
+    public void testCreateAndFindMethods() {
+        RegistryDTO dto = new RegistryDTO();
+        dto.setRegistryId("r1");
+        dto.setProtocol("http");
+        dto.setAddress("localhost:8848");
+        when(registryMapper.selectByRegistryId("r1")).thenReturn(null);
+        
when(registryMapper.selectAll()).thenReturn(Collections.singletonList(RegistryDO.builder().id("id").registryId("r1").build()));
+        assertNotNull(registryService.createOrUpdate(dto));
+        verify(registryMapper).insert(any(RegistryDO.class));
+        registryService.findById("id");
+        registryService.findByRegistryId("r1");
+        registryService.listAll();
+        verify(registryMapper).selectById("id");
+        verify(registryMapper, 
org.mockito.Mockito.times(2)).selectByRegistryId("r1");
+    }
+
+    @Test
+    public void testCreateRejectsDuplicateAndUpdateBranches() {
+        RegistryDTO dto = new RegistryDTO();
+        dto.setRegistryId("r1");
+        
when(registryMapper.selectByRegistryId("r1")).thenReturn(RegistryDO.builder().id("other").build());
+        assertThrows(ShenyuAdminException.class, () -> 
registryService.createOrUpdate(dto));
+
+        RegistryDTO missingId = new RegistryDTO();
+        missingId.setRegistryId("r1");
+        missingId.setId(null);
+        when(registryMapper.selectByRegistryId("r1")).thenReturn(null);
+        when(registryMapper.insert(any(RegistryDO.class))).thenReturn(1);
+        assertNotNull(registryService.createOrUpdate(missingId));
+
+        RegistryDTO update = new RegistryDTO();
+        update.setId("id");
+        update.setRegistryId("r1");
+        
when(registryMapper.selectByRegistryId("r1")).thenReturn(RegistryDO.builder().id("id").build());
+        
when(registryMapper.updateSelective(any(RegistryDO.class))).thenReturn(0);
+        assertNull(registryService.createOrUpdate(update));
+
+        
when(registryMapper.selectByRegistryId("r1")).thenReturn(RegistryDO.builder().id("other").build());
+        assertThrows(ShenyuAdminException.class, () -> 
registryService.createOrUpdate(update));
+        verify(registryMapper, never()).deleteByIds(any());
+    }
+
+    @Test
+    public void testUpdateSuccessReturnsRegistry() {
+        RegistryDTO update = new RegistryDTO();
+        update.setId("id");
+        update.setRegistryId("r1");
+        update.setProtocol("http");
+        update.setAddress("localhost:8848");
+        update.setNamespace("registry-namespace");
+        update.setGroup("group");
+        update.setUsername("user");
+        update.setPassword("password");
+        
when(registryMapper.selectByRegistryId("r1")).thenReturn(RegistryDO.builder().id("id").build());
+        
when(registryMapper.updateSelective(any(RegistryDO.class))).thenReturn(1);
+        RegistryVO result = registryService.createOrUpdate(update);
+        assertNotNull(result);
+        assertEquals("id", result.getId());
+        assertEquals("r1", result.getRegistryId());
+        assertEquals("http", result.getProtocol());
+        assertEquals("localhost:8848", result.getAddress());
+        assertEquals("registry-namespace", result.getNamespace());
+        assertEquals("group", result.getGroup());
+        assertEquals("user", result.getUsername());
+        assertEquals("password", result.getPassword());
+    }
+
+    @Test
+    public void testListAndDelete() {
+        when(registryMapper.countByQuery(any())).thenReturn(0);
+        org.apache.shenyu.admin.model.query.RegistryQuery query = new 
org.apache.shenyu.admin.model.query.RegistryQuery();
+        query.setPageParameter(new 
org.apache.shenyu.admin.model.page.PageParameter());
+        registryService.listByPage(query);
+        
assertEquals(org.apache.shenyu.admin.utils.ShenyuResultMessage.DELETE_SUCCESS,
+                registryService.delete(Collections.singletonList("id")));
+        verify(registryMapper).deleteByIds(any());
+    }
+
+    @Test
+    public void testListByPageWithResultsAndFindMappings() {
+        org.apache.shenyu.admin.model.query.RegistryQuery query = new 
org.apache.shenyu.admin.model.query.RegistryQuery();
+        query.setPageParameter(new 
org.apache.shenyu.admin.model.page.PageParameter());
+        RegistryDO registry = 
RegistryDO.builder().id("id").registryId("r1").build();
+        when(registryMapper.countByQuery(query)).thenReturn(1);
+        
when(registryMapper.selectByQuery(query)).thenReturn(Collections.singletonList(registry));
+        assertEquals(1, 
registryService.listByPage(query).getDataList().size());
+        when(registryMapper.selectById("id")).thenReturn(registry);
+        when(registryMapper.selectByRegistryId("r1")).thenReturn(registry);
+        assertEquals("id", registryService.findById("id").getId());
+        assertEquals("id", registryService.findByRegistryId("r1").getId());
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScalePolicyServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScalePolicyServiceImplTest.java
new file mode 100644
index 0000000000..7b52116d96
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScalePolicyServiceImplTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.shenyu.admin.service;
+
+import org.apache.shenyu.admin.mapper.ScalePolicyMapper;
+import org.apache.shenyu.admin.model.dto.ScalePolicyDTO;
+import org.apache.shenyu.admin.model.entity.ScalePolicyDO;
+import org.apache.shenyu.admin.scale.scaler.cache.ScalePolicyCache;
+import org.apache.shenyu.admin.scale.scaler.ScaleService;
+import org.apache.shenyu.admin.service.impl.ScalePolicyServiceImpl;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mock;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.transaction.support.TransactionSynchronizationUtils;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link ScalePolicyServiceImpl}. */
+@ExtendWith(MockitoExtension.class)
+public final class ScalePolicyServiceImplTest {
+    @Mock
+    private ScalePolicyMapper mapper;
+
+    @Mock
+    private ScalePolicyCache cache;
+
+    @Mock
+    private ScaleService scaleService;
+
+    @InjectMocks
+    private ScalePolicyServiceImpl service;
+
+    @AfterEach
+    public void tearDown() {
+        if (TransactionSynchronizationManager.isSynchronizationActive()) {
+            TransactionSynchronizationManager.clearSynchronization();
+        }
+    }
+
+    @Test
+    public void testSelectAndFind() {
+        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+        ScalePolicyDO policy = 
ScalePolicyDO.builder().id("p").dateCreated(timestamp).dateUpdated(timestamp).build();
+        when(mapper.selectAll()).thenReturn(Collections.singletonList(policy));
+        assertEquals("p", service.selectAll().get(0).getId());
+        when(mapper.selectByPrimaryKey("p")).thenReturn(policy);
+        assertEquals("p", service.findById("p").getId());
+        verify(mapper).selectByPrimaryKey("p");
+    }
+
+    @Test
+    public void testUpdateImmediatelyAndAfterCommit() {
+        
when(mapper.updateByPrimaryKeySelective(any(ScalePolicyDO.class))).thenReturn(1);
+        service.update(ScalePolicyDTO.builder().id("p").status(1).build());
+        verify(cache, times(1)).updatePolicy(any(ScalePolicyDO.class));
+        verify(scaleService).executeScaling();
+
+        TransactionSynchronizationManager.initSynchronization();
+        service.update(ScalePolicyDTO.builder().id("p2").status(0).build());
+        verify(cache, times(1)).updatePolicy(any(ScalePolicyDO.class));
+        
TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
+        verify(cache, times(2)).updatePolicy(any(ScalePolicyDO.class));
+        verify(scaleService, times(2)).executeScaling();
+    }
+
+    @Test
+    public void testSelectAllReturnsEmptyList() {
+        when(mapper.selectAll()).thenReturn(Collections.emptyList());
+        assertEquals(Collections.emptyList(), service.selectAll());
+    }
+
+    @Test
+    public void testUpdateSkipsPolicyApplicationWhenMapperReturnsZero() {
+        
when(mapper.updateByPrimaryKeySelective(any(ScalePolicyDO.class))).thenReturn(0);
+        assertEquals(0, 
service.update(ScalePolicyDTO.builder().id("p").status(1).build()));
+        verify(cache, 
org.mockito.Mockito.never()).updatePolicy(any(ScalePolicyDO.class));
+        verify(scaleService, org.mockito.Mockito.never()).executeScaling();
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScaleRuleServiceTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScaleRuleServiceTest.java
index 56c499d63e..a49815f7db 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScaleRuleServiceTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScaleRuleServiceTest.java
@@ -20,20 +20,26 @@ package org.apache.shenyu.admin.service;
 import org.apache.shenyu.admin.mapper.ScaleRuleMapper;
 import org.apache.shenyu.admin.model.dto.ScaleRuleDTO;
 import org.apache.shenyu.admin.model.entity.ScaleRuleDO;
+import org.apache.shenyu.admin.model.page.PageParameter;
+import org.apache.shenyu.admin.model.query.ScaleRuleQuery;
 import org.apache.shenyu.admin.scale.monitor.subject.cache.ScaleRuleCache;
 import org.apache.shenyu.admin.service.impl.ScaleRuleServiceImpl;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
+import org.junit.jupiter.api.Test;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mock;
 import 
org.springframework.transaction.support.TransactionSynchronizationManager;
 import org.springframework.transaction.support.TransactionSynchronizationUtils;
 
+import java.sql.Timestamp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 /**
@@ -82,4 +88,107 @@ public final class ScaleRuleServiceTest {
 
         verify(scaleRuleCache).addOrUpdateRuleToCache(any(ScaleRuleDO.class));
     }
+
+    @Test
+    public void testReadUpdateAndDelete() {
+        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+        
when(scaleRuleMapper.selectAll()).thenReturn(java.util.Collections.singletonList(ScaleRuleDO.builder().id("r")
+                .dateCreated(timestamp).dateUpdated(timestamp).build()));
+        scaleRuleService.selectAll();
+        scaleRuleService.findById("r");
+        ScaleRuleQuery query = new ScaleRuleQuery();
+        query.setPageParameter(new PageParameter());
+        
when(scaleRuleMapper.selectByQuery(query)).thenReturn(java.util.Collections.emptyList());
+        scaleRuleService.listByPage(query);
+        ScaleRuleDTO dto = new ScaleRuleDTO();
+        dto.setId("r");
+        dto.setMetricName("new");
+        
when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(ScaleRuleDO.builder().id("r").metricName("old").build(),
+                ScaleRuleDO.builder().id("r").metricName("new").build());
+        
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(1);
+        TransactionSynchronizationManager.initSynchronization();
+        scaleRuleService.update(dto);
+        
TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
+        verify(scaleRuleCache).removeRulesFromCache(any());
+        verify(scaleRuleCache).addOrUpdateRuleToCache(any(ScaleRuleDO.class));
+        TransactionSynchronizationManager.clearSynchronization();
+        when(scaleRuleMapper.delete(any())).thenReturn(1);
+        scaleRuleService.delete(java.util.Collections.singletonList("r"));
+        verify(scaleRuleCache).removeRulesByIdsFromCache(any());
+    }
+
+    @Test
+    public void 
testCreateOrUpdateDispatchesAndCreateSkipsCacheWhenInsertFails() {
+        
when(scaleRuleMapper.insertSelective(any(ScaleRuleDO.class))).thenReturn(0);
+        assertEquals(0, scaleRuleService.createOrUpdate(new ScaleRuleDTO()));
+        verifyNoInteractions(scaleRuleCache);
+        ScaleRuleDTO update = new ScaleRuleDTO();
+        update.setId("r");
+        ScaleRuleDO persisted = 
ScaleRuleDO.builder().id("r").metricName("metric").build();
+        when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(null, 
persisted);
+        
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(1);
+        assertEquals(1, scaleRuleService.createOrUpdate(update));
+        verify(scaleRuleMapper).insertSelective(any(ScaleRuleDO.class));
+        
verify(scaleRuleMapper).updateByPrimaryKeySelective(any(ScaleRuleDO.class));
+        verify(scaleRuleCache).addOrUpdateRuleToCache(persisted);
+    }
+
+    @Test
+    public void testReadMethodsWithResults() {
+        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+        ScaleRuleDO rule = ScaleRuleDO.builder().id("r").metricName("metric")
+                .dateCreated(timestamp).dateUpdated(timestamp).build();
+        ScaleRuleQuery query = new ScaleRuleQuery();
+        query.setPageParameter(new PageParameter());
+        
when(scaleRuleMapper.selectByQuery(query)).thenReturn(java.util.Collections.singletonList(rule));
+        assertEquals(1, 
scaleRuleService.listByPage(query).getDataList().size());
+        when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(rule);
+        assertEquals("r", scaleRuleService.findById("r").getId());
+    }
+
+    @Test
+    public void testUpdateAndDeleteSkipCacheWhenMapperReturnsZero() {
+        ScaleRuleDTO dto = new ScaleRuleDTO();
+        dto.setId("r");
+        when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(null);
+        
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(0);
+        assertEquals(0, scaleRuleService.update(dto));
+        when(scaleRuleMapper.delete(any())).thenReturn(0);
+        assertEquals(0, 
scaleRuleService.delete(java.util.Collections.singletonList("r")));
+        verifyNoInteractions(scaleRuleCache);
+    }
+
+    @Test
+    public void testUpdatePreservesCacheWhenMetricNameIsUnchanged() {
+        ScaleRuleDTO dto = new ScaleRuleDTO();
+        dto.setId("r");
+        dto.setMetricName("metric");
+        ScaleRuleDO persisted = 
ScaleRuleDO.builder().id("r").metricName("metric").build();
+        when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(persisted);
+        
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(1);
+        assertEquals(1, scaleRuleService.update(dto));
+        verify(scaleRuleCache, never()).removeRulesFromCache(any());
+        verify(scaleRuleCache).addOrUpdateRuleToCache(persisted);
+    }
+
+    @Test
+    public void testUpdateWithoutPreviousRuleAndDeleteAfterCommit() {
+        ScaleRuleDTO dto = new ScaleRuleDTO();
+        dto.setId("r");
+        when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(null,
+                ScaleRuleDO.builder().id("r").metricName("metric").build());
+        
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(1);
+        TransactionSynchronizationManager.initSynchronization();
+        scaleRuleService.update(dto);
+        
TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
+        verify(scaleRuleCache, never()).removeRulesFromCache(any());
+        verify(scaleRuleCache).addOrUpdateRuleToCache(any(ScaleRuleDO.class));
+        TransactionSynchronizationManager.clearSynchronization();
+        when(scaleRuleMapper.delete(any())).thenReturn(1);
+        TransactionSynchronizationManager.initSynchronization();
+        scaleRuleService.delete(java.util.Collections.singletonList("r"));
+        verify(scaleRuleCache, never()).removeRulesByIdsFromCache(any());
+        
TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
+        verify(scaleRuleCache).removeRulesByIdsFromCache(any());
+    }
 }
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/manager/RegisterApiDocServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/manager/RegisterApiDocServiceImplTest.java
new file mode 100644
index 0000000000..7db5de53eb
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/manager/RegisterApiDocServiceImplTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.shenyu.admin.service.manager;
+
+import org.apache.shenyu.admin.model.dto.ApiDTO;
+import org.apache.shenyu.admin.model.dto.TagDTO;
+import org.apache.shenyu.admin.model.vo.TagVO;
+import org.apache.shenyu.admin.service.ApiService;
+import org.apache.shenyu.admin.service.manager.impl.RegisterApiDocServiceImpl;
+import org.apache.shenyu.admin.service.TagService;
+import org.apache.shenyu.register.common.dto.ApiDocRegisterDTO;
+import org.apache.shenyu.register.common.enums.EventType;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mock;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link RegisterApiDocServiceImpl}. */
+@ExtendWith(MockitoExtension.class)
+public final class RegisterApiDocServiceImplTest {
+    @Mock
+    private ApiService apiService;
+
+    @Mock
+    private TagService tagService;
+
+    @InjectMocks
+    private RegisterApiDocServiceImpl service;
+
+    @Test
+    public void testRegisterUsesExistingAndCreatesMissingTags() {
+        ReflectionTestUtils.setField(service, "apiService", apiService);
+        ReflectionTestUtils.setField(service, "tagService", tagService);
+        ApiDocRegisterDTO dto = new ApiDocRegisterDTO();
+        dto.setEventType(EventType.REGISTER);
+        dto.setApiPath("/a");
+        dto.setHttpMethod(1);
+        dto.setRpcType("http");
+        dto.setTags(Collections.singletonList("tag"));
+        
when(tagService.findByQuery("tag")).thenReturn(Collections.emptyList());
+        service.registerApiDocument(dto);
+        verify(apiService).deleteByApiPathHttpMethodRpcType("/a", 1, "http");
+        verify(tagService).createRootTag(any(), any());
+        verify(apiService).createOrUpdate(any());
+    }
+
+    @Test
+    public void testRegisterWithExistingTagAndOffline() {
+        ReflectionTestUtils.setField(service, "apiService", apiService);
+        ReflectionTestUtils.setField(service, "tagService", tagService);
+        ApiDocRegisterDTO dto = new ApiDocRegisterDTO();
+        dto.setEventType(EventType.REGISTER);
+        dto.setContextPath("/ctx");
+        dto.setTags(Collections.singletonList("tag"));
+        TagVO tag = new TagVO();
+        tag.setId("tag-id");
+        
when(tagService.findByQuery("tag")).thenReturn(Collections.singletonList(tag));
+        service.registerApiDocument(dto);
+        verify(apiService).createOrUpdate(any());
+        ApiDocRegisterDTO offline = new ApiDocRegisterDTO();
+        offline.setEventType(EventType.OFFLINE);
+        offline.setContextPath("/ctx");
+        service.registerApiDocument(offline);
+        verify(apiService).offlineByContextPath("/ctx");
+    }
+
+    @Test
+    public void testRegisterUsesContextPathWhenTagsAreAbsent() {
+        ReflectionTestUtils.setField(service, "apiService", apiService);
+        ReflectionTestUtils.setField(service, "tagService", tagService);
+        ApiDocRegisterDTO dto = new ApiDocRegisterDTO();
+        dto.setEventType(EventType.REGISTER);
+        dto.setContextPath("/ctx");
+        
when(tagService.findByQuery("/ctx")).thenReturn(Collections.emptyList());
+        service.registerApiDocument(dto);
+        verify(tagService).createRootTag(any(), any());
+        verify(apiService).createOrUpdate(any());
+    }
+
+    @Test
+    public void testRegisterIgnoresOtherEventTypes() {
+        ReflectionTestUtils.setField(service, "apiService", apiService);
+        ReflectionTestUtils.setField(service, "tagService", tagService);
+        ApiDocRegisterDTO dto = new ApiDocRegisterDTO();
+        dto.setEventType(EventType.UPDATED);
+        service.registerApiDocument(dto);
+        org.mockito.Mockito.verifyNoInteractions(apiService, tagService);
+    }
+
+    @Test
+    public void testRegisterCombinesExistingAndNewTagsAndCopiesApiFields() {
+        ApiDocRegisterDTO dto = new ApiDocRegisterDTO();
+        dto.setEventType(EventType.REGISTER);
+        dto.setApiPath("/ctx/api");
+        dto.setContextPath("/ctx");
+        dto.setHttpMethod(1);
+        dto.setRpcType("http");
+        dto.setDocument("document");
+        dto.setApiSource(1);
+        dto.setApiOwner("owner");
+        dto.setApiDesc("description");
+        dto.setExt("{}");
+        dto.setVersion("v1");
+        dto.setConsume("application/json");
+        dto.setProduce("application/json");
+        dto.setState(1);
+        dto.setTags(List.of("existing", "new"));
+        TagVO tag = new TagVO();
+        tag.setId("existing-id");
+        
when(tagService.findByQuery("existing")).thenReturn(Collections.singletonList(tag));
+        
when(tagService.findByQuery("new")).thenReturn(Collections.emptyList());
+        service.registerApiDocument(dto);
+        ArgumentCaptor<TagDTO> tagCaptor = 
ArgumentCaptor.forClass(TagDTO.class);
+        verify(tagService).createRootTag(tagCaptor.capture(), 
org.mockito.ArgumentMatchers.isNull());
+        ArgumentCaptor<ApiDTO> apiCaptor = 
ArgumentCaptor.forClass(ApiDTO.class);
+        verify(apiService).createOrUpdate(apiCaptor.capture());
+        ApiDTO api = apiCaptor.getValue();
+        assertNotNull(tagCaptor.getValue().getId());
+        assertEquals(List.of("existing-id", tagCaptor.getValue().getId()), 
api.getTagIds());
+        assertEquals("new", tagCaptor.getValue().getTagName());
+        assertEquals("/ctx/api", api.getApiPath());
+        assertEquals("/ctx", api.getContextPath());
+        assertEquals(1, api.getHttpMethod());
+        assertEquals("http", api.getRpcType());
+        assertEquals("document", api.getDocument());
+        assertEquals(1, api.getApiSource());
+        assertEquals("owner", api.getApiOwner());
+        assertEquals("description", api.getApiDesc());
+        assertEquals("{}", api.getExt());
+        assertEquals("v1", api.getVersion());
+        assertEquals("application/json", api.getConsume());
+        assertEquals("application/json", api.getProduce());
+        assertEquals(1, api.getState());
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/register/ShenyuClientRegisterMcpServiceImplTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/register/ShenyuClientRegisterMcpServiceImplTest.java
new file mode 100644
index 0000000000..fc91e558c2
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/register/ShenyuClientRegisterMcpServiceImplTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.shenyu.admin.service.register;
+
+import org.apache.shenyu.admin.mapper.NamespacePluginRelMapper;
+import org.apache.shenyu.admin.mapper.PluginMapper;
+import org.apache.shenyu.admin.model.dto.RuleConditionDTO;
+import org.apache.shenyu.admin.model.dto.RuleDTO;
+import org.apache.shenyu.admin.model.entity.MetaDataDO;
+import org.apache.shenyu.admin.model.entity.PluginDO;
+import org.apache.shenyu.admin.service.MetaDataService;
+import org.apache.shenyu.admin.service.RuleService;
+import org.apache.shenyu.admin.service.SelectorService;
+import org.apache.shenyu.common.enums.MatchModeEnum;
+import org.apache.shenyu.common.enums.OperatorEnum;
+import org.apache.shenyu.common.enums.ParamTypeEnum;
+import org.apache.shenyu.common.enums.RpcTypeEnum;
+import org.apache.shenyu.common.utils.PluginNameAdapter;
+import org.apache.shenyu.register.common.dto.McpToolsRegisterDTO;
+import org.apache.shenyu.register.common.dto.MetaDataRegisterDTO;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mock;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link ShenyuClientRegisterMcpServiceImpl}. */
+@ExtendWith(MockitoExtension.class)
+public final class ShenyuClientRegisterMcpServiceImplTest {
+    @InjectMocks
+    private ShenyuClientRegisterMcpServiceImpl service;
+
+    @Mock
+    private MetaDataService metaDataService;
+
+    @Mock
+    private SelectorService selectorService;
+
+    @Mock
+    private RuleService ruleService;
+
+    @Mock
+    private PluginMapper pluginMapper;
+
+    @Mock
+    private NamespacePluginRelMapper namespacePluginRelMapper;
+
+    @Test
+    public void testRpcTypeAndRegisterTools() {
+        ReflectionTestUtils.setField(service, "metaDataService", 
metaDataService);
+        ReflectionTestUtils.setField(service, "selectorService", 
selectorService);
+        ReflectionTestUtils.setField(service, "ruleService", ruleService);
+        ShenyuClientRegisterMcpServiceImpl spy = 
org.mockito.Mockito.spy(service);
+        doNothing().when(spy).checkNamespacePluginRel(anyString(), 
anyString());
+        MetaDataRegisterDTO metadata = 
MetaDataRegisterDTO.builder().path("/tool").ruleName("tool")
+                .namespaceId("ns").contextPath("").build();
+        McpToolsRegisterDTO dto = new McpToolsRegisterDTO();
+        dto.setMetaDataRegisterDTO(metadata);
+        dto.setMcpConfig("{}");
+        dto.setNamespaceId("ns");
+        when(selectorService.registerDefault(any(), anyString(), 
anyString())).thenReturn("selector");
+        when(metaDataService.findByPathAndNamespaceId("/tool", 
"ns")).thenReturn(null);
+        spy.registerMcpTools(dto);
+        assertEquals(RpcTypeEnum.MCP.getName(), spy.rpcType());
+        verify(spy).checkNamespacePluginRel("ns", 
PluginNameAdapter.rpcTypeAdapter(RpcTypeEnum.MCP.getName()));
+        verify(selectorService).registerDefault(any(), anyString(), 
anyString());
+        ArgumentCaptor<RuleDTO> captor = 
ArgumentCaptor.forClass(RuleDTO.class);
+        verify(ruleService).registerDefault(captor.capture());
+        RuleDTO rule = captor.getValue();
+        assertEquals("selector", rule.getSelectorId());
+        assertEquals("tool", rule.getName());
+        assertEquals("{}", rule.getHandle());
+        assertEquals("ns", rule.getNamespaceId());
+        assertEquals(MatchModeEnum.AND.getCode(), rule.getMatchMode());
+        assertEquals(Boolean.TRUE, rule.getEnabled());
+        assertEquals(Boolean.TRUE, rule.getLoged());
+        assertEquals(Boolean.FALSE, rule.getMatchRestful());
+        assertEquals(1, rule.getSort());
+        assertEquals(1, rule.getRuleConditions().size());
+        RuleConditionDTO condition = rule.getRuleConditions().get(0);
+        assertEquals(ParamTypeEnum.URI.getName(), condition.getParamType());
+        assertEquals("/", condition.getParamName());
+        assertEquals("/**", condition.getParamValue());
+        assertEquals(OperatorEnum.MATCH.getAlias(), condition.getOperator());
+        verify(metaDataService).saveOrUpdateMetaData(null, metadata);
+    }
+
+    @Test
+    public void testMcpExtensionHandlersReturnEmptyImplementations() {
+        
assertNull(service.selectorHandler(MetaDataRegisterDTO.builder().build()));
+        assertNull(service.ruleHandler());
+        assertNull(service.buildHandle(java.util.Collections.emptyList(), new 
org.apache.shenyu.admin.model.entity.SelectorDO()));
+    }
+
+    @Test
+    public void testRegisterToolsUsesDefaultNamespaceAndContextPath() {
+        ReflectionTestUtils.setField(service, "metaDataService", 
metaDataService);
+        ReflectionTestUtils.setField(service, "selectorService", 
selectorService);
+        ReflectionTestUtils.setField(service, "ruleService", ruleService);
+        ShenyuClientRegisterMcpServiceImpl spy = 
org.mockito.Mockito.spy(service);
+        doNothing().when(spy).checkNamespacePluginRel(anyString(), 
anyString());
+        doNothing().when(spy).registerContextPath(any());
+        MetaDataRegisterDTO metadata = 
MetaDataRegisterDTO.builder().path("/tool").ruleName("tool")
+                
.namespaceId(org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_ID).contextPath("/ctx").build();
+        McpToolsRegisterDTO dto = new McpToolsRegisterDTO();
+        dto.setMetaDataRegisterDTO(metadata);
+        dto.setMcpConfig("{}");
+        when(selectorService.registerDefault(any(), anyString(), 
anyString())).thenReturn("selector");
+        MetaDataDO existing = MetaDataDO.builder().build();
+        when(metaDataService.findByPathAndNamespaceId("/tool", 
org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_ID))
+                .thenReturn(existing);
+        spy.registerMcpTools(dto);
+        
assertEquals(org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_ID,
 dto.getNamespaceId());
+        
verify(spy).checkNamespacePluginRel(org.apache.shenyu.common.constant.Constants.SYS_DEFAULT_NAMESPACE_ID,
+                
org.apache.shenyu.common.utils.PluginNameAdapter.rpcTypeAdapter(RpcTypeEnum.MCP.getName()));
+        verify(spy).registerContextPath(metadata);
+        verify(metaDataService).saveOrUpdateMetaData(existing, metadata);
+    }
+
+    @Test
+    public void testRegisterToolsStopsWhenPluginDoesNotExist() {
+        ReflectionTestUtils.setField(service, "pluginMapper", pluginMapper);
+        ReflectionTestUtils.setField(service, "namespacePluginRelMapper", 
namespacePluginRelMapper);
+        McpToolsRegisterDTO dto = new McpToolsRegisterDTO();
+        dto.setNamespaceId("ns");
+        dto.setMetaDataRegisterDTO(MetaDataRegisterDTO.builder().build());
+        assertThrows(IllegalArgumentException.class, () -> 
service.registerMcpTools(dto));
+        
verify(pluginMapper).selectByName(PluginNameAdapter.rpcTypeAdapter(RpcTypeEnum.MCP.getName()));
+        verifyNoInteractions(namespacePluginRelMapper, selectorService, 
ruleService, metaDataService);
+    }
+
+    @Test
+    public void testRegisterToolsStopsWhenPluginIsNotEnabledInNamespace() {
+        ReflectionTestUtils.setField(service, "pluginMapper", pluginMapper);
+        ReflectionTestUtils.setField(service, "namespacePluginRelMapper", 
namespacePluginRelMapper);
+        
when(pluginMapper.selectByName(PluginNameAdapter.rpcTypeAdapter(RpcTypeEnum.MCP.getName())))
+                .thenReturn(PluginDO.builder().id("plugin").build());
+        McpToolsRegisterDTO dto = new McpToolsRegisterDTO();
+        dto.setNamespaceId("ns");
+        dto.setMetaDataRegisterDTO(MetaDataRegisterDTO.builder().build());
+        assertThrows(IllegalArgumentException.class, () -> 
service.registerMcpTools(dto));
+        
verify(namespacePluginRelMapper).selectByPluginIdAndNamespaceId("plugin", "ns");
+        verifyNoInteractions(selectorService, ruleService, metaDataService);
+    }
+}
diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/alert/AlarmServiceImplTest.java
 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/alert/AlarmServiceImplTest.java
new file mode 100644
index 0000000000..8ac3158348
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/alert/AlarmServiceImplTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.shenyu.plugin.base.alert;
+
+import org.apache.shenyu.common.dto.AlarmContent;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.Mock;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Tests for {@link AlarmServiceImpl}. */
+@ExtendWith(MockitoExtension.class)
+public final class AlarmServiceImplTest {
+    @Mock
+    private RestTemplate restTemplate;
+
+    @Test
+    public void testDisabledAndMissingAdmins() {
+        AlarmContent content = new AlarmContent.Builder().build();
+        new AlarmServiceImpl(restTemplate, "localhost:9095", 
false).alarm(content);
+        new AlarmServiceImpl(restTemplate, "", true).alarm(content);
+        verifyNoPost();
+    }
+
+    @Test
+    public void testAlarmStopsAfterSuccessfulEndpoint() {
+        AlarmContent content = new AlarmContent.Builder().build();
+        
when(restTemplate.postForEntity(contains("localhost:9095/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class)))
+                .thenReturn(new ResponseEntity<>(HttpStatus.OK));
+        new AlarmServiceImpl(restTemplate, "localhost:9095,localhost:9096", 
true).alarm(content);
+        
verify(restTemplate).postForEntity(contains("localhost:9095/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class));
+        verify(restTemplate, 
never()).postForEntity(contains("localhost:9096/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class));
+    }
+
+    @Test
+    public void testAlarmContinuesOnFailureAndException() {
+        AlarmContent content = new AlarmContent.Builder().build();
+        
when(restTemplate.postForEntity(contains("localhost:9095/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class)))
+                .thenReturn(new ResponseEntity<>(HttpStatus.BAD_GATEWAY));
+        
when(restTemplate.postForEntity(contains("localhost:9096/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class)))
+                .thenThrow(new RuntimeException("down"));
+        new AlarmServiceImpl(restTemplate, "localhost:9095,localhost:9096", 
true).alarm(content);
+        verify(restTemplate, 
org.mockito.Mockito.times(2)).postForEntity(org.mockito.ArgumentMatchers.anyString(),
+                org.mockito.ArgumentMatchers.any(), eq(Void.class));
+    }
+
+    @Test
+    public void testAlarmSucceedsOnFallbackEndpoint() {
+        AlarmContent content = new AlarmContent.Builder().build();
+        
when(restTemplate.postForEntity(contains("localhost:9095/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class)))
+                .thenReturn(new ResponseEntity<>(HttpStatus.BAD_GATEWAY));
+        
when(restTemplate.postForEntity(contains("localhost:9096/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class)))
+                .thenReturn(new ResponseEntity<>(HttpStatus.OK));
+        new AlarmServiceImpl(restTemplate, 
"localhost:9095,localhost:9096,localhost:9097", true).alarm(content);
+        verify(restTemplate, 
org.mockito.Mockito.times(2)).postForEntity(org.mockito.ArgumentMatchers.anyString(),
+                org.mockito.ArgumentMatchers.any(), eq(Void.class));
+        verify(restTemplate, 
never()).postForEntity(contains("localhost:9097/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class));
+    }
+
+    @Test
+    public void testAlarmRecoversAfterEndpointThrowsException() {
+        AlarmContent content = new AlarmContent.Builder().build();
+        
when(restTemplate.postForEntity(contains("localhost:9095/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class)))
+                .thenThrow(new RuntimeException("unreachable"));
+        
when(restTemplate.postForEntity(contains("localhost:9096/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class)))
+                .thenReturn(new ResponseEntity<>(HttpStatus.OK));
+        new AlarmServiceImpl(restTemplate, 
"localhost:9095,localhost:9096,localhost:9097", true).alarm(content);
+        ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
+        
verify(restTemplate).postForEntity(contains("localhost:9096/alert/report"), 
captor.capture(), eq(Void.class));
+        HttpEntity<?> request = (HttpEntity<?>) captor.getValue();
+        assertSame(content, request.getBody());
+        assertEquals(MediaType.APPLICATION_JSON, 
request.getHeaders().getContentType());
+        verify(restTemplate, 
never()).postForEntity(contains("localhost:9097/alert/report"), 
org.mockito.ArgumentMatchers.any(), eq(Void.class));
+    }
+
+    private void verifyNoPost() {
+        verify(restTemplate, 
never()).postForEntity(org.mockito.ArgumentMatchers.anyString(), 
org.mockito.ArgumentMatchers.any(), eq(Void.class));
+    }
+}

Reply via email to