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

github-merge-queue[bot] pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 760c33a944 [Feature][Connector-V2] Add RabbitMQ source connectivity 
dry-run (#12432)
760c33a944 is described below

commit 760c33a94433db85b974ae4d6622d05b726a4a47
Author: Goutam Adwant <[email protected]>
AuthorDate: Wed Oct 7 07:37:01 2026 +0000

    [Feature][Connector-V2] Add RabbitMQ source connectivity dry-run (#12432)
---
 docs/en/connectors/source/Rabbitmq.md              |  13 ++
 docs/en/engines/zeta/user-command.md               |   1 +
 docs/zh/connectors/source/Rabbitmq.md              |  13 ++
 docs/zh/engines/zeta/user-command.md               |   1 +
 .../seatunnel/rabbitmq/client/RabbitmqClient.java  |   7 +-
 .../client/RabbitmqSourceDryRunValidator.java      | 128 ++++++++++++++
 .../seatunnel/rabbitmq/source/RabbitmqSource.java  |  59 +------
 .../rabbitmq/source/RabbitmqSourceFactory.java     |  40 ++++-
 .../rabbitmq/source/RabbitmqSourceSchema.java      |  95 +++++++++++
 .../rabbitmq/RabbitmqSourceFactoryTest.java        |  47 ++++++
 .../rabbitmq/client/RabbitmqClientTest.java        | 187 +++++++++++++++++++++
 .../e2e/connector/rabbitmq/RabbitmqIT.java         | 184 ++++++++++++++++++++
 12 files changed, 714 insertions(+), 61 deletions(-)

diff --git a/docs/en/connectors/source/Rabbitmq.md 
b/docs/en/connectors/source/Rabbitmq.md
index 1c50b4b0ba..f677459313 100644
--- a/docs/en/connectors/source/Rabbitmq.md
+++ b/docs/en/connectors/source/Rabbitmq.md
@@ -30,6 +30,19 @@ The source must be non-parallel (parallelism set to 1) in 
order to achieve exact
 
 :::
 
+## Connectivity dry-run
+
+Run `bin/seatunnel.sh --config <your-job.conf> --dry-run connect` with your 
own job configuration.
+
+This source supports connection/authentication and existing-queue metadata 
checks. It uses the same URI precedence, virtual host, TLS configuration and 
configured single-table or `tables_configs` schemas as the normal source, 
without creating a reader.
+
+- Each configured queue is checked with a passive declaration. No queue is 
created or deleted, no consumer is registered, and no message is read, 
acknowledged or published.
+- Queues must already exist, even when `passive = false`. A normal job may 
create a missing queue, but this dry-run deliberately cannot. Exclusive queues 
owned by another connection cannot be checked.
+- A successful check does **not** prove consumer/read permissions, queue 
declaration compatibility, payload format/schema compatibility, or runtime 
delivery guarantees. Passive declarations may refresh a queue's expiration 
timer.
+- Socket connection, AMQP handshake and channel RPC waits are capped at 10 
seconds each; a smaller positive `connection_timeout` is retained. Zero is 
bounded for this preflight. Queue checks are sequential, so this is not a 
10-second limit for the entire command. DNS resolution follows the JVM resolver.
+- Automatic/topology recovery is disabled only on the temporary connection. 
Cleanup aborts that connection with a one-second wait limit, including after 
failure. Normal source settings and behavior are unchanged.
+- Validation errors do not include raw connection strings, broker text or 
secret-bearing exception causes. Sink connectivity remains unsupported.
+
 ## Options
 
 | name                       | type    | required | default value |
diff --git a/docs/en/engines/zeta/user-command.md 
b/docs/en/engines/zeta/user-command.md
index 53e0ea4141..0b822ff845 100644
--- a/docs/en/engines/zeta/user-command.md
+++ b/docs/en/engines/zeta/user-command.md
@@ -83,6 +83,7 @@ The `--dry-run connect` option runs the static checks first, 
then uses connector
 | Jdbc      | Yes (connectivity + schema inference) | Yes (connectivity + 
table existence + field compatibility) |
 | Kafka     | Yes ([topic metadata + runtime output 
schema](../../connectors/source/Kafka.md#connectivity-dry-run), not 
consumer/group permissions) | No |
 | FakeSource | Yes (schema inference only, no external system) | - |
+| RabbitMQ | Yes ([existing-queue metadata + configured schema; not consumer 
permissions](../../connectors/source/Rabbitmq.md#connectivity-dry-run)) | No |
 | S3File | Yes (metadata connectivity + inline schema, single-table 
text/csv/json/xml; see [supported 
scope](../../connectors/source/S3File.md#connectivity-dry-run)) | - |
 | Redis | Yes ([connectivity + 
authentication](../../connectors/source/Redis.md#connectivity-dry-run) + 
configured schema; no key access) | Yes ([connectivity + 
authentication](../../connectors/sink/Redis.md#connectivity-dry-run); no field 
compatibility or write permission check) |
 
diff --git a/docs/zh/connectors/source/Rabbitmq.md 
b/docs/zh/connectors/source/Rabbitmq.md
index 5975930117..d53e2595ae 100644
--- a/docs/zh/connectors/source/Rabbitmq.md
+++ b/docs/zh/connectors/source/Rabbitmq.md
@@ -30,6 +30,19 @@ import ChangeLog from '../changelog/connector-rabbitmq.md';
 
 :::
 
+## 连接预检查
+
+使用自己的作业配置执行 `bin/seatunnel.sh --config <your-job.conf> --dry-run connect`。
+
+Source 支持连接、身份认证及已有队列的元数据检查。预检查复用正常 Source 的 URI 优先级、虚拟主机、TLS 配置,以及单表或 
`tables_configs` 的 schema 构建逻辑,不创建 reader。
+
+- 对每个配置队列执行被动声明,不创建或删除队列,不注册消费者,不读取、确认或发布消息。
+- 即使 `passive = false`,队列也必须事先存在。正常作业可以创建缺失队列,但预检查不会创建。其他连接持有的独占队列无法检查。
+- 检查成功**不代表**具备消费/读取权限、队列声明属性兼容、消息格式与 schema 匹配,或运行时交付保证。被动声明可能刷新队列的过期计时器。
+- Socket 连接、AMQP 握手及 channel RPC 各自最多等待 10 秒;保留更小的正数 
`connection_timeout`,零值在预检查中也受到限制。队列按顺序检查,因此整个命令并非最多 10 秒。DNS 解析由 JVM 解析器控制。
+- 仅临时连接禁用自动恢复和拓扑恢复。成功或失败后均终止该连接,清理等待上限为 1 秒。正常 Source 的配置及行为不变。
+- 校验错误不包含原始连接字符串、服务端原始错误或含凭据的异常原因。Sink 连通性仍不支持。
+
 ## 选项
 
 | 参数名                        | 类型      | 必须 | 默认值   | 描述                       
                                                   |
diff --git a/docs/zh/engines/zeta/user-command.md 
b/docs/zh/engines/zeta/user-command.md
index c491ac9cdd..134939bfaa 100644
--- a/docs/zh/engines/zeta/user-command.md
+++ b/docs/zh/engines/zeta/user-command.md
@@ -99,6 +99,7 @@ bin/seatunnel.sh --config 
$SEATUNNEL_HOME/config/v2.batch.config.template --dry-
 | Jdbc   | 支持(连通性 + schema 推断) | 支持(连通性 + 表存在性 + 字段兼容性) |
 | Kafka  | 支持([主题元数据 + 运行时输出 
schema](../../connectors/source/Kafka.md#连通性-dry-run),不含消费或消费组权限) | 不支持 |
 | FakeSource | 支持(仅 schema 推断,无外部系统) | - |
+| RabbitMQ | 支持([已有队列元数据 + 配置 
schema,不含消费权限](../../connectors/source/Rabbitmq.md#连接预检查)) | 不支持 |
 | S3File | 支持(元数据连通性 + 内联 schema,仅单表 
text/csv/json/xml;参见[支持范围](../../connectors/source/S3File.md#连接预检查)) | - |
 | Redis | 支持([连通性 + 认证](../../connectors/source/Redis.md#连通性-dry-run) + 配置的 
schema,不访问 key) | 支持([连通性 + 
认证](../../connectors/sink/Redis.md#连通性-dry-run),不校验字段兼容性和写入权限) |
 
diff --git 
a/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqClient.java
 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqClient.java
index af7ca2ef62..78dddc2d43 100644
--- 
a/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqClient.java
+++ 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqClient.java
@@ -62,7 +62,7 @@ public class RabbitmqClient implements AutoCloseable {
     public RabbitmqClient(RabbitmqConfig config) {
         this.config = config;
         try {
-            this.connectionFactory = createConnectionFactory();
+            this.connectionFactory = createConnectionFactory(config);
             this.connection = connectionFactory.newConnection();
             this.channel = connection.createChannel();
 
@@ -102,7 +102,8 @@ public class RabbitmqClient implements AutoCloseable {
         return new QueueingConsumer(channel, queue, splitId);
     }
 
-    private ConnectionFactory createConnectionFactory() {
+    // Shared by runtime and preflight so URI precedence, authentication and 
TLS stay identical.
+    static ConnectionFactory createConnectionFactory(RabbitmqConfig config) {
         ConnectionFactory factory = new ConnectionFactory();
         if (StringUtils.isNotEmpty(config.getUri())) {
             try {
@@ -159,7 +160,7 @@ public class RabbitmqClient implements AutoCloseable {
     }
 
     /** Configures TLS with the JVM trust store and hostname verification 
enabled. */
-    private void configureSsl(ConnectionFactory factory) {
+    private static void configureSsl(ConnectionFactory factory) {
         try {
             factory.useSslProtocol(SSLContext.getDefault());
             factory.enableHostnameVerification();
diff --git 
a/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqSourceDryRunValidator.java
 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqSourceDryRunValidator.java
new file mode 100644
index 0000000000..8d73e796b8
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqSourceDryRunValidator.java
@@ -0,0 +1,128 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.rabbitmq.client;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqConfig;
+import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqSourceOptions;
+
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.Connection;
+import com.rabbitmq.client.ConnectionFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+/** Checks existing queue metadata using an independently owned, 
non-recovering connection. */
+public final class RabbitmqSourceDryRunValidator {
+    private static final int MAX_TIMEOUT_MS = 10_000;
+    private static final int CLOSE_TIMEOUT_MS = 1_000;
+
+    private RabbitmqSourceDryRunValidator() {}
+
+    public static void validate(ReadonlyConfig options) throws Exception {
+        validate(options, RabbitmqClient::createConnectionFactory);
+    }
+
+    static void validate(
+            ReadonlyConfig options, Function<RabbitmqConfig, 
ConnectionFactory> factoryProvider)
+            throws Exception {
+        if (Thread.currentThread().isInterrupted()) {
+            throw new InterruptedException("RabbitMQ connect dry-run 
interrupted");
+        }
+        Connection connection = null;
+        Exception failure = null;
+        String stage = "connection configuration";
+        boolean interrupted = false;
+        try {
+            RabbitmqConfig config = new RabbitmqConfig(options);
+            ConnectionFactory factory = factoryProvider.apply(config);
+            // Zero means infinite in the client. Cap preflight waits without 
changing job options.
+            int configuredTimeout = factory.getConnectionTimeout();
+            if (configuredTimeout < 0) {
+                throw new IllegalArgumentException("Invalid connection 
timeout");
+            }
+            int timeout =
+                    configuredTimeout == 0
+                            ? MAX_TIMEOUT_MS
+                            : Math.min(configuredTimeout, MAX_TIMEOUT_MS);
+            factory.setConnectionTimeout(timeout);
+            factory.setHandshakeTimeout(timeout);
+            factory.setChannelRpcTimeout(timeout);
+            factory.setShutdownTimeout(CLOSE_TIMEOUT_MS);
+            factory.setAutomaticRecoveryEnabled(false);
+            factory.setTopologyRecoveryEnabled(false);
+
+            stage = "connection or authentication";
+            connection = factory.newConnection();
+            stage = "channel creation";
+            Channel channel = connection.createChannel();
+            if (channel == null) {
+                throw new IOException("No channel available");
+            }
+            List<Map<String, Object>> tables =
+                    options.getOptional(RabbitmqSourceOptions.TABLE_CONFIGS)
+                            .orElse(
+                                    Collections.singletonList(
+                                            Collections.singletonMap(
+                                                    
RabbitmqSourceOptions.QUEUE_NAME.key(),
+                                                    config.getQueueName())));
+            stage = "existing queue metadata (pre-create configured queues 
before dry-run)";
+            for (Map<String, Object> table : tables) {
+                if (Thread.currentThread().isInterrupted()) {
+                    throw new InterruptedException("RabbitMQ connect dry-run 
interrupted");
+                }
+                // Never use the runtime declaration path: passive=false may 
create queues.
+                channel.queueDeclarePassive(
+                        
ReadonlyConfig.fromMap(table).get(RabbitmqSourceOptions.QUEUE_NAME));
+            }
+        } catch (InterruptedException e) {
+            interrupted = true;
+            failure = new InterruptedException("RabbitMQ connect dry-run 
interrupted");
+        } catch (Exception e) {
+            // Driver/configuration errors can contain URI credentials or 
broker-controlled text.
+            failure = new IOException("RabbitMQ connect dry-run could not 
validate " + stage);
+        } finally {
+            interrupted |= Thread.interrupted();
+            if (connection != null) {
+                try {
+                    // Abort owns channel cleanup too, including a channel 
closed by a failed RPC.
+                    connection.abort(CLOSE_TIMEOUT_MS);
+                } catch (Exception e) {
+                    if (failure == null) {
+                        failure =
+                                new IOException(
+                                        "RabbitMQ connect dry-run connection 
cleanup failed");
+                    }
+                }
+            }
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+                if (failure == null) {
+                    failure = new InterruptedException("RabbitMQ connect 
dry-run interrupted");
+                }
+            }
+        }
+        if (failure != null) {
+            throw failure;
+        }
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSource.java
 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSource.java
index bbf045c82a..003ee25cb6 100644
--- 
a/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSource.java
+++ 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSource.java
@@ -20,18 +20,14 @@ package 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.source;
 import org.apache.seatunnel.api.common.JobContext;
 import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode;
 import org.apache.seatunnel.api.configuration.ReadonlyConfig;
-import org.apache.seatunnel.api.options.ConnectorCommonOptions;
 import org.apache.seatunnel.api.source.Boundedness;
 import org.apache.seatunnel.api.source.SeaTunnelSource;
 import org.apache.seatunnel.api.source.SourceReader;
 import org.apache.seatunnel.api.source.SourceSplitEnumerator;
 import org.apache.seatunnel.api.source.SupportParallelism;
 import org.apache.seatunnel.api.table.catalog.CatalogTable;
-import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
-import org.apache.seatunnel.api.table.catalog.TableIdentifier;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.common.constants.JobMode;
-import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqBaseOptions;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqConfig;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.exception.RabbitmqConnectorException;
 import org.apache.seatunnel.connectors.seatunnel.rabbitmq.split.RabbitmqSplit;
@@ -64,60 +60,9 @@ public class RabbitmqSource
         initializeCatalogTables(config);
     }
 
-    /**
-     * Parses the configuration to initialize the CatalogTables. Determines 
whether the source is
-     * operating in Single-Table or Multi-Table mode.
-     *
-     * @param config The plugin configuration.
-     */
     private void initializeCatalogTables(ReadonlyConfig config) {
-        boolean hasTableConfigs =
-                
config.getOptional(ConnectorCommonOptions.TABLE_CONFIGS).isPresent();
-        boolean hasSchema = 
config.getOptional(ConnectorCommonOptions.SCHEMA).isPresent();
-
-        if (hasTableConfigs) {
-            // Multi-Table Mode: Parse multiple queue configurations
-            List<Map<String, Object>> tableConfigList =
-                    config.get(ConnectorCommonOptions.TABLE_CONFIGS);
-            for (Map<String, Object> item : tableConfigList) {
-                ReadonlyConfig tableConfig = ReadonlyConfig.fromMap(item);
-                CatalogTable table = buildCatalogTable(tableConfig);
-                String queueName = 
tableConfig.get(RabbitmqBaseOptions.QUEUE_NAME);
-
-                this.catalogTables.add(table);
-                this.queueToTableMap.put(queueName, table);
-            }
-        } else if (hasSchema) {
-            CatalogTable table = buildCatalogTable(config);
-            String queueName = config.get(RabbitmqBaseOptions.QUEUE_NAME);
-            if (queueName == null) {
-                queueName = rabbitmqConfig.getQueueName();
-            }
-            this.catalogTables.add(table);
-            this.queueToTableMap.put(queueName, table);
-        }
-    }
-
-    private CatalogTable buildCatalogTable(ReadonlyConfig config) {
-        CatalogTable catalogTable = CatalogTableUtil.buildWithConfig(config);
-        Map<String, String> options = new HashMap<>(catalogTable.getOptions());
-        options.put(
-                RabbitmqBaseOptions.FORMAT.key(), 
config.get(RabbitmqBaseOptions.FORMAT).name());
-        config.getOptional(RabbitmqBaseOptions.PROTOBUF_SCHEMA)
-                .ifPresent(value -> 
options.put(RabbitmqBaseOptions.PROTOBUF_SCHEMA.key(), value));
-        config.getOptional(RabbitmqBaseOptions.PROTOBUF_MESSAGE_NAME)
-                .ifPresent(
-                        value ->
-                                options.put(
-                                        
RabbitmqBaseOptions.PROTOBUF_MESSAGE_NAME.key(), value));
-        return CatalogTable.of(
-                TableIdentifier.of(catalogTable.getCatalogName(), 
catalogTable.getTablePath()),
-                catalogTable.getTableSchema(),
-                options,
-                catalogTable.getPartitionKeys(),
-                catalogTable.getComment(),
-                catalogTable.getCatalogName(),
-                catalogTable.getMetadataSchema());
+        RabbitmqSourceSchema.initializeCatalogTables(
+                config, rabbitmqConfig, catalogTables, queueToTableMap);
     }
 
     @Override
diff --git 
a/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSourceFactory.java
 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSourceFactory.java
index 969a3880b8..2a2c4b73ed 100644
--- 
a/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSourceFactory.java
+++ 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSourceFactory.java
@@ -18,14 +18,19 @@
 package org.apache.seatunnel.connectors.seatunnel.rabbitmq.source;
 
 import org.apache.seatunnel.api.configuration.util.Conditions;
+import org.apache.seatunnel.api.configuration.util.ConfigValidator;
 import org.apache.seatunnel.api.configuration.util.OptionRule;
 import org.apache.seatunnel.api.source.SeaTunnelSource;
 import org.apache.seatunnel.api.source.SourceSplit;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.connector.TableSource;
 import org.apache.seatunnel.api.table.factory.Factory;
+import org.apache.seatunnel.api.table.factory.SupportSourceDryRunValidation;
 import org.apache.seatunnel.api.table.factory.TableSourceFactory;
 import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
+import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.client.RabbitmqSourceDryRunValidator;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqBaseOptions;
+import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqConfig;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqMessageFormat;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqSingleTableValidator;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqSinkOptions;
@@ -34,10 +39,43 @@ import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqTableCo
 
 import com.google.auto.service.AutoService;
 
+import java.io.IOException;
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 
 @AutoService(Factory.class)
-public class RabbitmqSourceFactory implements TableSourceFactory {
+public class RabbitmqSourceFactory implements TableSourceFactory, 
SupportSourceDryRunValidation {
+
+    /** Reuses runtime catalog construction without opening a reader or a 
network connection. */
+    @Override
+    public List<CatalogTable> inferSchemaForDryRun(TableSourceFactoryContext 
context)
+            throws IOException {
+        try {
+            ConfigValidator.of(context.getOptions()).validate(optionRule());
+            List<CatalogTable> tables = new ArrayList<>();
+            RabbitmqSourceSchema.initializeCatalogTables(
+                    context.getOptions(),
+                    new RabbitmqConfig(context.getOptions()),
+                    tables,
+                    new HashMap<>());
+            return tables;
+        } catch (RuntimeException e) {
+            throw new IOException(
+                    "RabbitMQ connect dry-run source configuration or schema 
is invalid");
+        }
+    }
+
+    /** Checks connectivity only, without starting the source's data-reading 
lifecycle. */
+    @Override
+    public void validateConnectionForDryRun(
+            TableSourceFactoryContext context, List<CatalogTable> 
catalogTables) throws Exception {
+        // The connection hook can be called directly, without the preceding 
schema hook.
+        inferSchemaForDryRun(context);
+        RabbitmqSourceDryRunValidator.validate(context.getOptions());
+    }
+
     @Override
     public String factoryIdentifier() {
         return "RabbitMQ";
diff --git 
a/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSourceSchema.java
 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSourceSchema.java
new file mode 100644
index 0000000000..5a72c0cf76
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-rabbitmq/src/main/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/source/RabbitmqSourceSchema.java
@@ -0,0 +1,95 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.rabbitmq.source;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.options.ConnectorCommonOptions;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.catalog.TableIdentifier;
+import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqBaseOptions;
+import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqConfig;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Pure schema construction shared by the source runtime and connectivity 
preflight. */
+final class RabbitmqSourceSchema {
+    private RabbitmqSourceSchema() {}
+
+    /**
+     * Parses the configuration to initialize the CatalogTables. Determines 
whether the source is
+     * operating in Single-Table or Multi-Table mode.
+     *
+     * @param config The plugin configuration.
+     */
+    static void initializeCatalogTables(
+            ReadonlyConfig config,
+            RabbitmqConfig rabbitmqConfig,
+            List<CatalogTable> catalogTables,
+            Map<String, CatalogTable> queueToTableMap) {
+        boolean hasTableConfigs =
+                
config.getOptional(ConnectorCommonOptions.TABLE_CONFIGS).isPresent();
+        boolean hasSchema = 
config.getOptional(ConnectorCommonOptions.SCHEMA).isPresent();
+
+        if (hasTableConfigs) {
+            // Multi-Table Mode: Parse multiple queue configurations
+            List<Map<String, Object>> tableConfigList =
+                    config.get(ConnectorCommonOptions.TABLE_CONFIGS);
+            for (Map<String, Object> item : tableConfigList) {
+                ReadonlyConfig tableConfig = ReadonlyConfig.fromMap(item);
+                CatalogTable table = buildCatalogTable(tableConfig);
+                String queueName = 
tableConfig.get(RabbitmqBaseOptions.QUEUE_NAME);
+
+                catalogTables.add(table);
+                queueToTableMap.put(queueName, table);
+            }
+        } else if (hasSchema) {
+            CatalogTable table = buildCatalogTable(config);
+            String queueName = config.get(RabbitmqBaseOptions.QUEUE_NAME);
+            if (queueName == null) {
+                queueName = rabbitmqConfig.getQueueName();
+            }
+            catalogTables.add(table);
+            queueToTableMap.put(queueName, table);
+        }
+    }
+
+    private static CatalogTable buildCatalogTable(ReadonlyConfig config) {
+        CatalogTable catalogTable = CatalogTableUtil.buildWithConfig(config);
+        Map<String, String> options = new HashMap<>(catalogTable.getOptions());
+        options.put(
+                RabbitmqBaseOptions.FORMAT.key(), 
config.get(RabbitmqBaseOptions.FORMAT).name());
+        config.getOptional(RabbitmqBaseOptions.PROTOBUF_SCHEMA)
+                .ifPresent(value -> 
options.put(RabbitmqBaseOptions.PROTOBUF_SCHEMA.key(), value));
+        config.getOptional(RabbitmqBaseOptions.PROTOBUF_MESSAGE_NAME)
+                .ifPresent(
+                        value ->
+                                options.put(
+                                        
RabbitmqBaseOptions.PROTOBUF_MESSAGE_NAME.key(), value));
+        return CatalogTable.of(
+                TableIdentifier.of(catalogTable.getCatalogName(), 
catalogTable.getTablePath()),
+                catalogTable.getTableSchema(),
+                options,
+                catalogTable.getPartitionKeys(),
+                catalogTable.getComment(),
+                catalogTable.getCatalogName(),
+                catalogTable.getMetadataSchema());
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-rabbitmq/src/test/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/RabbitmqSourceFactoryTest.java
 
b/seatunnel-connectors-v2/connector-rabbitmq/src/test/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/RabbitmqSourceFactoryTest.java
index 4588e83d30..537379f26f 100644
--- 
a/seatunnel-connectors-v2/connector-rabbitmq/src/test/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/RabbitmqSourceFactoryTest.java
+++ 
b/seatunnel-connectors-v2/connector-rabbitmq/src/test/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/RabbitmqSourceFactoryTest.java
@@ -23,13 +23,18 @@ import 
org.apache.seatunnel.api.configuration.util.OptionRule;
 import org.apache.seatunnel.api.configuration.util.OptionValidationException;
 import org.apache.seatunnel.api.configuration.util.RequiredOption;
 import org.apache.seatunnel.api.options.table.TableSchemaOptions;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.factory.SupportSourceDryRunValidation;
+import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqMessageFormat;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqSourceOptions;
+import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.source.RabbitmqSource;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.source.RabbitmqSourceFactory;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -37,6 +42,48 @@ import java.util.Map;
 
 public class RabbitmqSourceFactoryTest {
 
+    @Test
+    void dryRunSchemaMatchesRuntimeForAllFormatsAndTableModes() throws 
Exception {
+        RabbitmqSourceFactory factory = new RabbitmqSourceFactory();
+        for (RabbitmqMessageFormat format : RabbitmqMessageFormat.values()) {
+            for (Map<String, Object> options :
+                    Arrays.asList(createValidSingleTableConfig(), 
createValidMultiTableConfig())) {
+                Map<String, Object> table =
+                        options.containsKey("tables_configs")
+                                ? getFirstTableConfig(options)
+                                : options;
+                table.put("format", format);
+                if (format == RabbitmqMessageFormat.PROTOBUF) {
+                    table.put(
+                            "protobuf_schema",
+                            "syntax = \"proto3\"; message Record { int32 id = 
1; }");
+                    table.put("protobuf_message_name", "Record");
+                }
+                TableSourceFactoryContext context =
+                        new TableSourceFactoryContext(
+                                ReadonlyConfig.fromMap(options), 
getClass().getClassLoader());
+                List<CatalogTable> expected =
+                        new 
RabbitmqSource(context.getOptions()).getProducedCatalogTables();
+                List<CatalogTable> actual = 
factory.inferSchemaForDryRun(context);
+                Assertions.assertEquals(expected.size(), actual.size());
+                for (int i = 0; i < expected.size(); i++) {
+                    Assertions.assertEquals(
+                            expected.get(i).getTableId(), 
actual.get(i).getTableId());
+                    Assertions.assertEquals(
+                            expected.get(i).getSeaTunnelRowType(),
+                            actual.get(i).getSeaTunnelRowType());
+                    Assertions.assertEquals(
+                            expected.get(i).getOptions(), 
actual.get(i).getOptions());
+                }
+            }
+        }
+    }
+
+    @Test
+    void supportsConnectivityDryRun() {
+        Assertions.assertTrue(new RabbitmqSourceFactory() instanceof 
SupportSourceDryRunValidation);
+    }
+
     @Test
     public void testFactoryIdentifier() {
         RabbitmqSourceFactory factory = new RabbitmqSourceFactory();
diff --git 
a/seatunnel-connectors-v2/connector-rabbitmq/src/test/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqClientTest.java
 
b/seatunnel-connectors-v2/connector-rabbitmq/src/test/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqClientTest.java
index a2ab086521..c0504fbe8c 100644
--- 
a/seatunnel-connectors-v2/connector-rabbitmq/src/test/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqClientTest.java
+++ 
b/seatunnel-connectors-v2/connector-rabbitmq/src/test/java/org/apache/seatunnel/connectors/seatunnel/rabbitmq/client/RabbitmqClientTest.java
@@ -17,25 +17,212 @@
 
 package org.apache.seatunnel.connectors.seatunnel.rabbitmq.client;
 
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqConfig;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.exception.RabbitmqConnectorException;
 
 import org.junit.jupiter.api.Test;
 
 import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.Connection;
+import com.rabbitmq.client.ConnectionFactory;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 class RabbitmqClientTest {
 
+    @Test
+    void dryRunOnlyPassivelyChecksAllQueuesAndClosesConnection() throws 
Exception {
+        ConnectionFactory factory = mock(ConnectionFactory.class);
+        Connection connection = mock(Connection.class);
+        Channel channel = mock(Channel.class);
+        when(factory.newConnection()).thenReturn(connection);
+        when(connection.createChannel()).thenReturn(channel);
+        Map<String, Object> options = dryRunOptions();
+        options.remove("queue_name");
+        options.put(
+                "tables_configs",
+                Arrays.asList(
+                        Collections.singletonMap("queue_name", "first"),
+                        Collections.singletonMap("queue_name", "second")));
+
+        
RabbitmqSourceDryRunValidator.validate(ReadonlyConfig.fromMap(options), ignored 
-> factory);
+
+        verify(channel).queueDeclarePassive("first");
+        verify(channel).queueDeclarePassive("second");
+        verifyNoMoreInteractions(channel);
+        verify(connection).createChannel();
+        verify(connection).abort(1000);
+        verifyNoMoreInteractions(connection);
+        verify(factory).setConnectionTimeout(10000);
+        verify(factory).setHandshakeTimeout(10000);
+        verify(factory).setChannelRpcTimeout(10000);
+        verify(factory).setAutomaticRecoveryEnabled(false);
+        verify(factory).setTopologyRecoveryEnabled(false);
+        assertFalse((Boolean) options.get("passive"));
+    }
+
+    @Test
+    void dryRunClosesConnectionWhenChannelCreationFailsWithoutExposingCause() 
throws Exception {
+        ConnectionFactory factory = mock(ConnectionFactory.class);
+        Connection connection = mock(Connection.class);
+        when(factory.newConnection()).thenReturn(connection);
+        when(connection.createChannel()).thenThrow(new 
IOException("amqp://user:secret@host"));
+        IOException failure =
+                assertThrows(
+                        IOException.class,
+                        () ->
+                                RabbitmqSourceDryRunValidator.validate(
+                                        
ReadonlyConfig.fromMap(dryRunOptions()),
+                                        ignored -> factory));
+        assertTrue(failure.getMessage().contains("channel creation"));
+        assertFalse(failure.getMessage().contains("secret"));
+        assertNull(failure.getCause());
+        assertEquals(0, failure.getSuppressed().length);
+        verify(connection).abort(1000);
+    }
+
+    @Test
+    void dryRunMissingQueueFailsWithoutCreatingItAndClosesConnection() throws 
Exception {
+        ConnectionFactory factory = mock(ConnectionFactory.class);
+        Connection connection = mock(Connection.class);
+        Channel channel = mock(Channel.class);
+        when(factory.newConnection()).thenReturn(connection);
+        when(connection.createChannel()).thenReturn(channel);
+        when(channel.queueDeclarePassive("existing"))
+                .thenThrow(new IOException("private broker text"));
+        IOException failure =
+                assertThrows(
+                        IOException.class,
+                        () ->
+                                RabbitmqSourceDryRunValidator.validate(
+                                        
ReadonlyConfig.fromMap(dryRunOptions()),
+                                        ignored -> factory));
+        assertTrue(failure.getMessage().contains("pre-create"));
+        assertNull(failure.getCause());
+        verify(channel).queueDeclarePassive("existing");
+        verifyNoMoreInteractions(channel);
+        verify(connection).abort(1000);
+    }
+
+    @Test
+    void dryRunHonorsSmallerTimeoutAndRejectsPreInterruptedThread() throws 
Exception {
+        ConnectionFactory factory = mock(ConnectionFactory.class);
+        when(factory.getConnectionTimeout()).thenReturn(250);
+        when(factory.newConnection()).thenThrow(new 
IOException("unavailable"));
+        assertThrows(
+                IOException.class,
+                () ->
+                        RabbitmqSourceDryRunValidator.validate(
+                                ReadonlyConfig.fromMap(dryRunOptions()), 
ignored -> factory));
+        verify(factory).setConnectionTimeout(250);
+        verify(factory).setHandshakeTimeout(250);
+        verify(factory).setChannelRpcTimeout(250);
+
+        ConnectionFactory unused = mock(ConnectionFactory.class);
+        Thread.currentThread().interrupt();
+        try {
+            assertThrows(
+                    InterruptedException.class,
+                    () ->
+                            RabbitmqSourceDryRunValidator.validate(
+                                    ReadonlyConfig.fromMap(dryRunOptions()), 
ignored -> unused));
+            assertTrue(Thread.currentThread().isInterrupted());
+            verifyNoInteractions(unused);
+        } finally {
+            Thread.interrupted();
+        }
+    }
+
+    @Test
+    void sharedFactoryPreservesUriPrecedenceAndRecoverySettings() {
+        Map<String, Object> options = dryRunOptions();
+        options.put("url", 
"amqp://uri-user:uri-password@uri-host:1234/my-vhost");
+        options.put("AUTOMATIC_RECOVERY_ENABLED", true);
+        RabbitmqConfig config = new 
RabbitmqConfig(ReadonlyConfig.fromMap(options));
+        ConnectionFactory factory = 
RabbitmqClient.createConnectionFactory(config);
+        assertEquals("uri-host", factory.getHost());
+        assertEquals(1234, factory.getPort());
+        assertEquals("uri-user", factory.getUsername());
+        assertEquals("uri-password", factory.getPassword());
+        assertEquals("my-vhost", factory.getVirtualHost());
+        assertTrue(factory.isAutomaticRecoveryEnabled());
+    }
+
+    private static Map<String, Object> dryRunOptions() {
+        Map<String, Object> options = new HashMap<>();
+        options.put("host", "localhost");
+        options.put("port", 5672);
+        options.put("username", "guest");
+        options.put("password", "guest");
+        options.put("queue_name", "existing");
+        options.put("passive", false);
+        return options;
+    }
+
+    @Test
+    void dryRunCleanupFailureIsSanitizedAndCannotPass() throws Exception {
+        ConnectionFactory factory = mock(ConnectionFactory.class);
+        Connection connection = mock(Connection.class);
+        Channel channel = mock(Channel.class);
+        when(factory.newConnection()).thenReturn(connection);
+        when(connection.createChannel()).thenReturn(channel);
+        doThrow(new 
IllegalStateException("private-secret")).when(connection).abort(1000);
+        IOException failure =
+                assertThrows(
+                        IOException.class,
+                        () ->
+                                RabbitmqSourceDryRunValidator.validate(
+                                        
ReadonlyConfig.fromMap(dryRunOptions()),
+                                        ignored -> factory));
+        assertTrue(failure.getMessage().contains("cleanup"));
+        assertFalse(failure.getMessage().contains("private-secret"));
+        assertNull(failure.getCause());
+    }
+
+    @Test
+    void dryRunRestoresInterruptAndClosesAfterQueueCheck() throws Exception {
+        ConnectionFactory factory = mock(ConnectionFactory.class);
+        Connection connection = mock(Connection.class);
+        Channel channel = mock(Channel.class);
+        when(factory.newConnection()).thenReturn(connection);
+        when(connection.createChannel()).thenReturn(channel);
+        when(channel.queueDeclarePassive("existing"))
+                .thenAnswer(
+                        ignored -> {
+                            Thread.currentThread().interrupt();
+                            return null;
+                        });
+        try {
+            assertThrows(
+                    InterruptedException.class,
+                    () ->
+                            RabbitmqSourceDryRunValidator.validate(
+                                    ReadonlyConfig.fromMap(dryRunOptions()), 
ignored -> factory));
+            assertTrue(Thread.currentThread().isInterrupted());
+            verify(connection).abort(1000);
+        } finally {
+            Thread.interrupted();
+        }
+    }
+
     @Test
     void declaresExistingQueuePassivelyWhenConfigured() throws Exception {
         Channel channel = mock(Channel.class);
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rabbitmq-e2e/src/test/java/org/apache/seatunnel/e2e/connector/rabbitmq/RabbitmqIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rabbitmq-e2e/src/test/java/org/apache/seatunnel/e2e/connector/rabbitmq/RabbitmqIT.java
index faa1d58c3f..5ef10bdeed 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rabbitmq-e2e/src/test/java/org/apache/seatunnel/e2e/connector/rabbitmq/RabbitmqIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-rabbitmq-e2e/src/test/java/org/apache/seatunnel/e2e/connector/rabbitmq/RabbitmqIT.java
@@ -17,10 +17,18 @@
 
 package org.apache.seatunnel.e2e.connector.rabbitmq;
 
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory;
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigRenderOptions;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
 import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.catalog.PhysicalColumn;
 import org.apache.seatunnel.api.table.catalog.TableIdentifier;
 import org.apache.seatunnel.api.table.catalog.TableSchema;
+import org.apache.seatunnel.api.table.factory.FactoryUtil;
+import org.apache.seatunnel.api.table.factory.SupportSourceDryRunValidation;
+import org.apache.seatunnel.api.table.factory.TableSourceFactory;
+import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
 import org.apache.seatunnel.api.table.type.ArrayType;
 import org.apache.seatunnel.api.table.type.BasicType;
 import org.apache.seatunnel.api.table.type.DecimalType;
@@ -33,6 +41,9 @@ import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.client.RabbitmqClient;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.config.RabbitmqConfig;
 import 
org.apache.seatunnel.connectors.seatunnel.rabbitmq.source.DeliveryMessage;
+import org.apache.seatunnel.core.starter.seatunnel.args.ClientCommandArgs;
+import 
org.apache.seatunnel.core.starter.seatunnel.command.SeaTunnelConfValidateCommand;
+import org.apache.seatunnel.core.starter.utils.CommandLineUtils;
 import org.apache.seatunnel.e2e.common.TestResource;
 import org.apache.seatunnel.e2e.common.TestSuiteBase;
 import org.apache.seatunnel.e2e.common.container.TestContainer;
@@ -43,7 +54,9 @@ import 
org.apache.seatunnel.format.protobuf.ProtobufSerializationSchema;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.io.TempDir;
 import org.testcontainers.containers.Container;
 import org.testcontainers.containers.GenericContainer;
 import org.testcontainers.containers.output.Slf4jLogConsumer;
@@ -53,13 +66,17 @@ import 
org.testcontainers.shaded.org.apache.commons.lang3.tuple.Pair;
 import org.testcontainers.utility.DockerImageName;
 import org.testcontainers.utility.DockerLoggerFactory;
 
+import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
+import com.rabbitmq.client.ConnectionFactory;
 import com.rabbitmq.client.DefaultConsumer;
 import lombok.extern.slf4j.Slf4j;
 
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.Duration;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -131,6 +148,173 @@ public class RabbitmqIT extends TestSuiteBase implements 
TestResource {
         }
     }
 
+    @Test
+    public void testDryRunLeavesMessagesAndConsumersUnchanged() throws 
Exception {
+        try (Connection connection = dryRunConnection();
+                Channel channel = connection.createChannel()) {
+            String queue =
+                    channel.queueDeclare(
+                                    "dry-run-" + java.util.UUID.randomUUID(),
+                                    false,
+                                    false,
+                                    false,
+                                    null)
+                            .getQueue();
+            channel.confirmSelect();
+            channel.basicPublish("", queue, null, 
"unchanged".getBytes(StandardCharsets.UTF_8));
+            channel.waitForConfirmsOrDie(5000);
+            int before = channel.queueDeclarePassive(queue).getMessageCount();
+            Map<String, Object> options = dryRunOptions(queue);
+            // Even passive=false must not use the runtime queue creation path.
+            options.put("passive", false);
+            validateDryRun(options);
+            Assertions.assertEquals(before, 
channel.queueDeclarePassive(queue).getMessageCount());
+            Assertions.assertEquals(0, 
channel.queueDeclarePassive(queue).getConsumerCount());
+            Assertions.assertArrayEquals(
+                    "unchanged".getBytes(StandardCharsets.UTF_8),
+                    channel.basicGet(queue, true).getBody());
+        }
+    }
+
+    @Test
+    public void testDryRunMultiQueueAndUriPrecedence() throws Exception {
+        try (Connection connection = dryRunConnection();
+                Channel channel = connection.createChannel()) {
+            String first =
+                    channel.queueDeclare(
+                                    "dry-run-" + java.util.UUID.randomUUID(),
+                                    false,
+                                    false,
+                                    false,
+                                    null)
+                            .getQueue();
+            String second =
+                    channel.queueDeclare(
+                                    "dry-run-" + java.util.UUID.randomUUID(),
+                                    false,
+                                    false,
+                                    false,
+                                    null)
+                            .getQueue();
+            Map<String, Object> options = dryRunOptions(first);
+            Object schema = options.remove("schema");
+            options.remove("queue_name");
+            List<Map<String, Object>> tables = new ArrayList<>();
+            for (String queue : Arrays.asList(first, second)) {
+                Map<String, Object> table = new HashMap<>();
+                table.put("queue_name", queue);
+                table.put("schema", schema);
+                tables.add(table);
+            }
+            options.put("tables_configs", tables);
+            options.put(
+                    "url",
+                    String.format(
+                            "amqp://guest:guest@%s:%s/%%2f",
+                            rabbitmqContainer.getHost(), 
rabbitmqContainer.getMappedPort(PORT)));
+            options.put("host", "unused.invalid");
+            options.put("port", 1);
+            validateDryRun(options);
+        }
+    }
+
+    @Test
+    public void testDryRunMissingQueueIsNotCreated() throws Exception {
+        String queue = "missing-dry-run-" + java.util.UUID.randomUUID();
+        Assertions.assertThrows(IOException.class, () -> 
validateDryRun(dryRunOptions(queue)));
+        try (Connection connection = dryRunConnection()) {
+            // A failed passive declaration closes its channel; only the 
connection needs closing.
+            Channel channel = connection.createChannel();
+            Assertions.assertThrows(IOException.class, () -> 
channel.queueDeclarePassive(queue));
+        }
+    }
+
+    @Test
+    public void testDryRunRejectsInvalidCredentialsWithoutEchoingThem() {
+        Map<String, Object> options = dryRunOptions("unused");
+        options.put("password", "dry-run-private-password");
+        Exception failure =
+                Assertions.assertThrows(IOException.class, () -> 
validateDryRun(options));
+        
Assertions.assertFalse(failure.toString().contains("dry-run-private-password"));
+        Assertions.assertNull(failure.getCause());
+    }
+
+    private Connection dryRunConnection() throws Exception {
+        ConnectionFactory factory = new ConnectionFactory();
+        factory.setHost(rabbitmqContainer.getHost());
+        factory.setPort(rabbitmqContainer.getMappedPort(PORT));
+        factory.setUsername(USERNAME);
+        factory.setPassword(PASSWORD);
+        return factory.newConnection();
+    }
+
+    private Map<String, Object> dryRunOptions(String queue) {
+        Map<String, Object> options = new HashMap<>();
+        options.put("host", rabbitmqContainer.getHost());
+        options.put("port", rabbitmqContainer.getMappedPort(PORT));
+        options.put("username", USERNAME);
+        options.put("password", PASSWORD);
+        options.put("queue_name", queue);
+        options.put(
+                "schema",
+                Collections.singletonMap("fields", 
Collections.singletonMap("name", "string")));
+        return options;
+    }
+
+    @Test
+    public void testDryRunCommand(@TempDir Path directory) throws Exception {
+        try (Connection connection = dryRunConnection();
+                Channel channel = connection.createChannel()) {
+            String queue =
+                    channel.queueDeclare(
+                                    "cli-dry-run-" + 
java.util.UUID.randomUUID(),
+                                    false,
+                                    false,
+                                    false,
+                                    null)
+                            .getQueue();
+            Map<String, Object> options = dryRunOptions(queue);
+            checkDryRunCommand(options, directory);
+            Assertions.assertEquals(0, 
channel.queueDeclarePassive(queue).getConsumerCount());
+        }
+    }
+
+    private void checkDryRunCommand(Map<String, Object> options, Path 
directory) throws Exception {
+        options.put("plugin_name", "RabbitMQ");
+        options.put("plugin_output", "preflight");
+        Map<String, Object> sink = new HashMap<>();
+        sink.put("plugin_name", "Console");
+        sink.put("plugin_input", "preflight");
+        Map<String, Object> job = new HashMap<>();
+        job.put("source", Collections.singletonList(options));
+        job.put("sink", Collections.singletonList(sink));
+        Path file = directory.resolve("dry-run.json");
+        Files.write(
+                file,
+                ConfigFactory.parseMap(job)
+                        .root()
+                        .render(ConfigRenderOptions.concise())
+                        .getBytes(StandardCharsets.UTF_8));
+        ClientCommandArgs args =
+                CommandLineUtils.parse(
+                        new String[] {"-c", file.toString(), "--dry-run", 
"connect"},
+                        new ClientCommandArgs(),
+                        "seatunnel.sh",
+                        true);
+        new SeaTunnelConfValidateCommand(args).execute();
+    }
+
+    private void validateDryRun(Map<String, Object> options) throws Exception {
+        ClassLoader loader = getClass().getClassLoader();
+        TableSourceFactory factory =
+                FactoryUtil.discoverFactory(loader, TableSourceFactory.class, 
"RabbitMQ");
+        Assertions.assertTrue(factory instanceof 
SupportSourceDryRunValidation);
+        SupportSourceDryRunValidation validator = 
(SupportSourceDryRunValidation) factory;
+        TableSourceFactoryContext context =
+                new TableSourceFactoryContext(ReadonlyConfig.fromMap(options), 
loader);
+        validator.validateConnectionForDryRun(context, 
validator.inferSchemaForDryRun(context));
+    }
+
     private static Pair<SeaTunnelRowType, List<SeaTunnelRow>> 
generateTestDataSet() {
 
         SeaTunnelRowType rowType =

Reply via email to