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

RongtongJin pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/rocketmq.git


The following commit(s) were added to refs/heads/develop by this push:
     new 66362b9c9f fix(auth): address ACL follow-up regressions (#11009)
66362b9c9f is described below

commit 66362b9c9f013f3ee7e510f5dbfb792c2053d3bd
Author: dingshuangxi888 <[email protected]>
AuthorDate: Tue Sep 8 09:42:40 2026 +0800

    fix(auth): address ACL follow-up regressions (#11009)
    
    Co-authored-by: shuangxi.dsx <[email protected]>
---
 .../DefaultAuthorizationContextBuilder.java        |   6 +-
 .../DefaultAuthorizationContextBuilderTest.java    |   6 ++
 .../broker/processor/AdminBrokerProcessor.java     |  17 ++-
 .../AdminBrokerProcessorConfigSanitizeTest.java    |  33 ++++--
 .../container/BrokerContainerProcessor.java        |  17 ++-
 ...BrokerContainerProcessorConfigSanitizeTest.java |  33 ++++--
 .../apache/rocketmq/remoting/Configuration.java    |  33 ++++--
 .../rocketmq/remoting/ConfigurationTest.java       |  46 +++++++++
 .../tools/command/auth/CopyUsersSubCommand.java    |  21 +++-
 .../command/auth/CopyUsersSubCommandTest.java      | 115 +++++++++++++++++++++
 10 files changed, 272 insertions(+), 55 deletions(-)

diff --git 
a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
 
b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
index af090b5f0f..54625ca527 100644
--- 
a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
+++ 
b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
@@ -257,8 +257,10 @@ public class DefaultAuthorizationContextBuilder implements 
AuthorizationContextB
                     }
                     break;
                 case RequestCode.VIEW_MESSAGE_BY_ID:
-                    topic = 
Resource.ofTopic(requireResource(fields.get(TOPIC), "topic"));
-                    result.add(DefaultAuthorizationContext.of(subject, topic, 
Action.GET, sourceIp));
+                    String viewTopic = requireResource(fields.get(TOPIC), 
"topic");
+                    Resource viewResource = 
NamespaceUtil.isRetryTopic(viewTopic)
+                        ? Resource.ofGroup(viewTopic) : 
Resource.ofTopic(viewTopic);
+                    result.add(DefaultAuthorizationContext.of(subject, 
viewResource, Action.GET, sourceIp));
                     break;
                 case RequestCode.CONSUMER_SEND_MSG_BACK:
                     group = 
Resource.ofGroup(requireResource(fields.get(GROUP), "consumer group"));
diff --git 
a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
 
b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
index f90352ef09..8be1f126d9 100644
--- 
a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
+++ 
b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
@@ -827,6 +827,12 @@ public class DefaultAuthorizationContextBuilderTest {
         assertResourceOrder(result, "Topic:viewTopic");
         assertActions(result, "Topic:viewTopic", Action.GET);
 
+        viewMessageHeader.setTopic("%RETRY%retryGroup");
+        result = builder.build(channelHandlerContext,
+            remotingRequest(RequestCode.VIEW_MESSAGE_BY_ID, viewMessageHeader, 
null));
+        assertResourceOrder(result, "Group:retryGroup");
+        assertActions(result, "Group:retryGroup", Action.GET);
+
         BatchAck firstAck = batchAck("topicA", "groupA", "0");
         BatchAck duplicateAck = batchAck("topicA", "groupA", "0");
         BatchAck secondAck = batchAck("topicB", "groupB", "0");
diff --git 
a/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
 
b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
index 602a8efce0..8083d7307c 100644
--- 
a/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
+++ 
b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
@@ -1178,7 +1178,8 @@ public class AdminBrokerProcessor implements 
NettyRequestProcessor {
         final RemotingCommand response = 
RemotingCommand.createResponseCommand(GetBrokerConfigResponseHeader.class);
         final GetBrokerConfigResponseHeader responseHeader = 
(GetBrokerConfigResponseHeader) response.readCustomHeader();
 
-        String content = 
sanitizeConfigForResponse(this.brokerController.getConfiguration().getAllConfigsFormatString());
+        String content = sanitizeConfigForResponse(
+            this.brokerController.getConfiguration().getAllConfigsSnapshot());
         if (content == null) {
             LOGGER.error("AdminBrokerProcessor#getBrokerConfig: failed to 
sanitize broker config, caller={}",
                 RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
@@ -1213,18 +1214,14 @@ public class AdminBrokerProcessor implements 
NettyRequestProcessor {
     };
 
     /**
-     * Remove sensitive entries from the exported config content. Returns null 
when the
-     * content cannot be parsed, so callers must fail closed instead of 
returning the
-     * original content.
+     * Remove sensitive entries from a snapshot of the broker configuration.
      */
-    static String sanitizeConfigForResponse(String content) {
-        if (content == null || content.isEmpty()) {
-            return content;
-        }
-        Properties properties = MixAll.string2Properties(content);
-        if (properties == null) {
+    static String sanitizeConfigForResponse(Properties source) {
+        if (source == null) {
             return null;
         }
+        Properties properties = new Properties();
+        properties.putAll(source);
         for (String key : SENSITIVE_CONFIG_KEYS) {
             properties.remove(key);
         }
diff --git 
a/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorConfigSanitizeTest.java
 
b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorConfigSanitizeTest.java
index be78a8908f..0a51449c3f 100644
--- 
a/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorConfigSanitizeTest.java
+++ 
b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorConfigSanitizeTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.rocketmq.broker.processor;
 
+import java.util.Properties;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
@@ -27,29 +28,39 @@ public class AdminBrokerProcessorConfigSanitizeTest {
 
     @Test
     public void testSanitizeRemovesSensitiveKeys() {
-        String content = "brokerName=broker-a\n"
-            + 
"initAuthenticationUser={\"username\":\"rocketmq\",\"password\":\"secret\"}\n"
-            + 
"innerClientAuthenticationCredentials={\"accessKey\":\"ak\",\"secretKey\":\"sk\"}\n"
-            + "listenPort=10911\n";
+        Properties properties = new Properties();
+        properties.setProperty("brokerName", "broker-a");
+        properties.setProperty("initAuthenticationUser", 
"sensitive-user-config");
+        properties.setProperty("innerClientAuthenticationCredentials", 
"sensitive-client-config");
+        properties.setProperty("listenPort", "10911");
 
-        String sanitized = 
AdminBrokerProcessor.sanitizeConfigForResponse(content);
+        String sanitized = 
AdminBrokerProcessor.sanitizeConfigForResponse(properties);
 
         assertFalse(sanitized.contains("initAuthenticationUser"));
         
assertFalse(sanitized.contains("innerClientAuthenticationCredentials"));
-        assertFalse(sanitized.contains("secret"));
+        assertFalse(sanitized.contains("sensitive"));
         assertTrue(sanitized.contains("brokerName=broker-a"));
         assertTrue(sanitized.contains("listenPort=10911"));
+        assertEquals(4, properties.size());
     }
 
     @Test
-    public void testSanitizeFailsClosedOnUnparsableContent() {
-        // malformed unicode escape makes Properties.load throw
-        
assertNull(AdminBrokerProcessor.sanitizeConfigForResponse("key=\\uZZZZ\n"));
+    public void testSanitizePreservesConfigValues() {
+        Properties properties = new Properties();
+        properties.setProperty("customPath", "C:\\rocketmq\\store");
+        properties.setProperty("label", "\u4e2d\u6587");
+        properties.setProperty("regex", "^foo\\d+$");
+
+        String sanitized = 
AdminBrokerProcessor.sanitizeConfigForResponse(properties);
+
+        assertTrue(sanitized.contains("customPath=C:\\rocketmq\\store"));
+        assertTrue(sanitized.contains("label=\u4e2d\u6587"));
+        assertTrue(sanitized.contains("regex=^foo\\d+$"));
     }
 
     @Test
-    public void testSanitizePassesThroughNullOrEmpty() {
+    public void testSanitizeHandlesNullOrEmpty() {
         assertNull(AdminBrokerProcessor.sanitizeConfigForResponse(null));
-        assertEquals("", AdminBrokerProcessor.sanitizeConfigForResponse(""));
+        assertEquals("", AdminBrokerProcessor.sanitizeConfigForResponse(new 
Properties()));
     }
 }
diff --git 
a/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java
 
b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java
index 5d534965fe..b4a6b145a4 100644
--- 
a/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java
+++ 
b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java
@@ -286,18 +286,14 @@ public class BrokerContainerProcessor implements 
NettyRequestProcessor {
     };
 
     /**
-     * Remove sensitive entries from the exported config content. Returns null 
when the
-     * content cannot be parsed, so callers must fail closed instead of 
returning the
-     * original content.
+     * Remove sensitive entries from a snapshot of the broker configuration.
      */
-    static String sanitizeConfigForResponse(String content) {
-        if (content == null || content.isEmpty()) {
-            return content;
-        }
-        Properties properties = MixAll.string2Properties(content);
-        if (properties == null) {
+    static String sanitizeConfigForResponse(Properties source) {
+        if (source == null) {
             return null;
         }
+        Properties properties = new Properties();
+        properties.putAll(source);
         for (String key : SENSITIVE_CONFIG_KEYS) {
             properties.remove(key);
         }
@@ -318,7 +314,8 @@ public class BrokerContainerProcessor implements 
NettyRequestProcessor {
         final RemotingCommand response = 
RemotingCommand.createResponseCommand(GetBrokerConfigResponseHeader.class);
         final GetBrokerConfigResponseHeader responseHeader = 
(GetBrokerConfigResponseHeader) response.readCustomHeader();
 
-        String content = 
sanitizeConfigForResponse(this.brokerContainer.getConfiguration().getAllConfigsFormatString());
+        String content = sanitizeConfigForResponse(
+            this.brokerContainer.getConfiguration().getAllConfigsSnapshot());
         if (content == null) {
             LOGGER.error("BrokerContainerProcessor#getBrokerConfig: failed to 
sanitize broker config");
             response.setCode(ResponseCode.SYSTEM_ERROR);
diff --git 
a/container/src/test/java/org/apache/rocketmq/container/BrokerContainerProcessorConfigSanitizeTest.java
 
b/container/src/test/java/org/apache/rocketmq/container/BrokerContainerProcessorConfigSanitizeTest.java
index 0aaa6d2c69..051b6dee2a 100644
--- 
a/container/src/test/java/org/apache/rocketmq/container/BrokerContainerProcessorConfigSanitizeTest.java
+++ 
b/container/src/test/java/org/apache/rocketmq/container/BrokerContainerProcessorConfigSanitizeTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.rocketmq.container;
 
+import java.util.Properties;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
@@ -27,29 +28,39 @@ public class BrokerContainerProcessorConfigSanitizeTest {
 
     @Test
     public void testSanitizeRemovesSensitiveKeys() {
-        String content = "brokerName=broker-a\n"
-            + 
"initAuthenticationUser={\"username\":\"rocketmq\",\"password\":\"secret\"}\n"
-            + 
"innerClientAuthenticationCredentials={\"accessKey\":\"ak\",\"secretKey\":\"sk\"}\n"
-            + "listenPort=10911\n";
+        Properties properties = new Properties();
+        properties.setProperty("brokerName", "broker-a");
+        properties.setProperty("initAuthenticationUser", 
"sensitive-user-config");
+        properties.setProperty("innerClientAuthenticationCredentials", 
"sensitive-client-config");
+        properties.setProperty("listenPort", "10911");
 
-        String sanitized = 
BrokerContainerProcessor.sanitizeConfigForResponse(content);
+        String sanitized = 
BrokerContainerProcessor.sanitizeConfigForResponse(properties);
 
         assertFalse(sanitized.contains("initAuthenticationUser"));
         
assertFalse(sanitized.contains("innerClientAuthenticationCredentials"));
-        assertFalse(sanitized.contains("secret"));
+        assertFalse(sanitized.contains("sensitive"));
         assertTrue(sanitized.contains("brokerName=broker-a"));
         assertTrue(sanitized.contains("listenPort=10911"));
+        assertEquals(4, properties.size());
     }
 
     @Test
-    public void testSanitizeFailsClosedOnUnparsableContent() {
-        // malformed unicode escape makes Properties.load throw
-        
assertNull(BrokerContainerProcessor.sanitizeConfigForResponse("key=\\uZZZZ\n"));
+    public void testSanitizePreservesConfigValues() {
+        Properties properties = new Properties();
+        properties.setProperty("customPath", "C:\\rocketmq\\store");
+        properties.setProperty("label", "\u4e2d\u6587");
+        properties.setProperty("regex", "^foo\\d+$");
+
+        String sanitized = 
BrokerContainerProcessor.sanitizeConfigForResponse(properties);
+
+        assertTrue(sanitized.contains("customPath=C:\\rocketmq\\store"));
+        assertTrue(sanitized.contains("label=\u4e2d\u6587"));
+        assertTrue(sanitized.contains("regex=^foo\\d+$"));
     }
 
     @Test
-    public void testSanitizePassesThroughNullOrEmpty() {
+    public void testSanitizeHandlesNullOrEmpty() {
         assertNull(BrokerContainerProcessor.sanitizeConfigForResponse(null));
-        assertEquals("", 
BrokerContainerProcessor.sanitizeConfigForResponse(""));
+        assertEquals("", 
BrokerContainerProcessor.sanitizeConfigForResponse(new Properties()));
     }
 }
diff --git 
a/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java 
b/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java
index 5b3e5ca379..649d59b83b 100644
--- a/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java
+++ b/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java
@@ -278,24 +278,39 @@ public class Configuration {
         return null;
     }
 
+    public Properties getAllConfigsSnapshot() {
+        try {
+            readWriteLock.readLock().lockInterruptibly();
+
+            try {
+                refreshAllConfigs();
+                Properties snapshot = new Properties();
+                snapshot.putAll(this.allConfigs);
+                return snapshot;
+            } finally {
+                readWriteLock.readLock().unlock();
+            }
+        } catch (InterruptedException e) {
+            log.error("getAllConfigsSnapshot lock error");
+        }
+
+        return null;
+    }
+
     private String getAllConfigsInternal() {
-        StringBuilder stringBuilder = new StringBuilder();
+        refreshAllConfigs();
+        return MixAll.properties2String(this.allConfigs, true);
+    }
 
-        // reload from config object ?
+    private void refreshAllConfigs() {
         for (Object configObject : this.configObjectList) {
             Properties properties = MixAll.object2Properties(configObject);
             if (properties != null) {
                 merge(properties, this.allConfigs);
             } else {
-                log.warn("getAllConfigsInternal object2Properties is null, 
{}", configObject.getClass());
+                log.warn("refreshAllConfigs object2Properties is null, {}", 
configObject.getClass());
             }
         }
-
-        {
-            stringBuilder.append(MixAll.properties2String(this.allConfigs, 
true));
-        }
-
-        return stringBuilder.toString();
     }
 
     private String getClientConfigsInternal(List<String> clientConigKeys) {
diff --git 
a/remoting/src/test/java/org/apache/rocketmq/remoting/ConfigurationTest.java 
b/remoting/src/test/java/org/apache/rocketmq/remoting/ConfigurationTest.java
new file mode 100644
index 0000000000..2698e8e72e
--- /dev/null
+++ b/remoting/src/test/java/org/apache/rocketmq/remoting/ConfigurationTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.remoting;
+
+import java.util.Properties;
+import org.apache.rocketmq.logging.org.slf4j.Logger;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.mockito.Mockito.mock;
+
+public class ConfigurationTest {
+
+    @Test
+    public void testGetAllConfigsSnapshotRefreshesAndCopiesProperties() {
+        TestConfig testConfig = new TestConfig();
+        Configuration configuration = new Configuration(mock(Logger.class), 
testConfig);
+        testConfig.customPath = "C:\\rocketmq\\store";
+
+        Properties snapshot = configuration.getAllConfigsSnapshot();
+
+        assertEquals("C:\\rocketmq\\store", 
snapshot.getProperty("customPath"));
+        assertNotSame(configuration.getAllConfigs(), snapshot);
+        snapshot.remove("customPath");
+        assertEquals("C:\\rocketmq\\store", 
configuration.getAllConfigs().getProperty("customPath"));
+    }
+
+    private static class TestConfig {
+        private String customPath = "initial";
+    }
+}
diff --git 
a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java
 
b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java
index 7f2c224ad8..703db77395 100644
--- 
a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java
+++ 
b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java
@@ -64,7 +64,7 @@ public class CopyUsersSubCommand implements SubCommand {
     public void execute(CommandLine commandLine, Options options,
         RPCHook rpcHook) throws SubCommandException {
 
-        DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook);
+        DefaultMQAdminExt defaultMQAdminExt = createAdminExt(rpcHook);
         
defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis()));
 
         try {
@@ -84,7 +84,16 @@ public class CopyUsersSubCommand implements SubCommand {
                         }
                     }
                 } else {
-                    userInfos = defaultMQAdminExt.listUser(sourceBroker, null);
+                    List<UserInfo> listedUserInfos = 
defaultMQAdminExt.listUser(sourceBroker, null);
+                    if (CollectionUtils.isNotEmpty(listedUserInfos)) {
+                        for (UserInfo listedUserInfo : listedUserInfos) {
+                            UserInfo userInfo = defaultMQAdminExt.getUser(
+                                sourceBroker, listedUserInfo.getUsername());
+                            if (userInfo != null) {
+                                userInfos.add(userInfo);
+                            }
+                        }
+                    }
                 }
 
                 if (CollectionUtils.isEmpty(userInfos)) {
@@ -92,6 +101,10 @@ public class CopyUsersSubCommand implements SubCommand {
                 }
 
                 for (UserInfo userInfo : userInfos) {
+                    if (StringUtils.isBlank(userInfo.getPassword())) {
+                        throw new IllegalStateException("Password is 
unavailable for user "
+                            + userInfo.getUsername() + ". Use a source broker 
super user to copy users.");
+                    }
                     if (defaultMQAdminExt.getUser(targetBroker, 
userInfo.getUsername()) == null) {
                         defaultMQAdminExt.createUser(targetBroker, userInfo);
                     } else {
@@ -110,4 +123,8 @@ public class CopyUsersSubCommand implements SubCommand {
             defaultMQAdminExt.shutdown();
         }
     }
+
+    DefaultMQAdminExt createAdminExt(RPCHook rpcHook) {
+        return new DefaultMQAdminExt(rpcHook);
+    }
 }
diff --git 
a/tools/src/test/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommandTest.java
 
b/tools/src/test/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommandTest.java
new file mode 100644
index 0000000000..fb10029e5e
--- /dev/null
+++ 
b/tools/src/test/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommandTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.tools.command.auth;
+
+import java.util.Collections;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Options;
+import org.apache.rocketmq.remoting.RPCHook;
+import org.apache.rocketmq.remoting.protocol.body.UserInfo;
+import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
+import org.apache.rocketmq.tools.command.SubCommandException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class CopyUsersSubCommandTest {
+    private static final String SOURCE_BROKER = "127.0.0.1:10911";
+    private static final String TARGET_BROKER = "127.0.0.1:20911";
+
+    private DefaultMQAdminExt adminExt;
+    private CommandLine commandLine;
+    private CopyUsersSubCommand command;
+
+    @Before
+    public void setUp() {
+        adminExt = mock(DefaultMQAdminExt.class);
+        commandLine = mock(CommandLine.class);
+        command = new CopyUsersSubCommand() {
+            @Override
+            DefaultMQAdminExt createAdminExt(RPCHook rpcHook) {
+                return adminExt;
+            }
+        };
+        when(commandLine.hasOption("f")).thenReturn(true);
+        when(commandLine.hasOption("t")).thenReturn(true);
+        when(commandLine.getOptionValue("f")).thenReturn(SOURCE_BROKER);
+        when(commandLine.getOptionValue("t")).thenReturn(TARGET_BROKER);
+    }
+
+    @Test
+    public void testCopyAllFetchesCompleteUserBeforeCreate() throws Exception {
+        UserInfo summary = UserInfo.of("alice", null, "Normal", "Enable");
+        UserInfo complete = UserInfo.of("alice", "dummy-password", "Normal", 
"Enable");
+        when(adminExt.listUser(SOURCE_BROKER, 
null)).thenReturn(Collections.singletonList(summary));
+        when(adminExt.getUser(SOURCE_BROKER, "alice")).thenReturn(complete);
+        when(adminExt.getUser(TARGET_BROKER, "alice")).thenReturn(null);
+
+        command.execute(commandLine, new Options(), null);
+
+        verify(adminExt).getUser(SOURCE_BROKER, "alice");
+        verify(adminExt).createUser(TARGET_BROKER, complete);
+    }
+
+    @Test
+    public void testCopyAllFetchesCompleteUserBeforeUpdate() throws Exception {
+        UserInfo summary = UserInfo.of("alice", null, "Normal", "Enable");
+        UserInfo complete = UserInfo.of("alice", "dummy-password", "Normal", 
"Enable");
+        UserInfo target = UserInfo.of("alice", null, "Normal", "Enable");
+        when(adminExt.listUser(SOURCE_BROKER, 
null)).thenReturn(Collections.singletonList(summary));
+        when(adminExt.getUser(SOURCE_BROKER, "alice")).thenReturn(complete);
+        when(adminExt.getUser(TARGET_BROKER, "alice")).thenReturn(target);
+
+        command.execute(commandLine, new Options(), null);
+
+        verify(adminExt).getUser(SOURCE_BROKER, "alice");
+        verify(adminExt).updateUser(TARGET_BROKER, complete);
+    }
+
+    @Test
+    public void testCopySelectedUserStillUsesCompleteUser() throws Exception {
+        UserInfo complete = UserInfo.of("alice", "dummy-password", "Normal", 
"Enable");
+        when(commandLine.getOptionValue('u')).thenReturn("alice");
+        when(adminExt.getUser(SOURCE_BROKER, "alice")).thenReturn(complete);
+        when(adminExt.getUser(TARGET_BROKER, "alice")).thenReturn(null);
+
+        command.execute(commandLine, new Options(), null);
+
+        verify(adminExt, never()).listUser(SOURCE_BROKER, null);
+        verify(adminExt).createUser(TARGET_BROKER, complete);
+    }
+
+    @Test
+    public void testCopyAllFailsWhenPasswordIsUnavailable() throws Exception {
+        UserInfo summary = UserInfo.of("alice", null, "Normal", "Enable");
+        when(adminExt.listUser(SOURCE_BROKER, 
null)).thenReturn(Collections.singletonList(summary));
+        when(adminExt.getUser(SOURCE_BROKER, "alice")).thenReturn(summary);
+
+        SubCommandException exception = 
Assert.assertThrows(SubCommandException.class,
+            () -> command.execute(commandLine, new Options(), null));
+
+        Assert.assertTrue(exception.getCause().getMessage().contains("Password 
is unavailable for user alice"));
+        verify(adminExt, never()).getUser(TARGET_BROKER, "alice");
+        verify(adminExt, never()).createUser(TARGET_BROKER, summary);
+        verify(adminExt, never()).updateUser(TARGET_BROKER, summary);
+    }
+}

Reply via email to