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

mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 2ddee091b9 [#12566] feat(core): Dispatch events for catalog connection 
tests (#12585)
2ddee091b9 is described below

commit 2ddee091b9eba4f54542d1e7f48c8cddb7e92b7d
Author: Vedant Madane <[email protected]>
AuthorDate: Wed Oct 28 07:09:05 2026 +0530

    [#12566] feat(core): Dispatch events for catalog connection tests (#12585)
    
    ### What changes were proposed in this pull request?
    
    Dispatch pre, success, and failure events for all three catalog
    connection-test overloads: pre-create, stored configuration, and
    proposed changes. Map `TEST_CONNECTION_CATALOG` in both audit
    implementations.
    
    Events retain the supplied pre-create catalog information or proposed
    changes, and failure events retain the original exception, following
    existing catalog event conventions. A private helper centralizes event
    dispatch and captures the username once.
    
    ### Why are the changes needed?
    
    Allow listeners and auditing integrations to observe catalog connection
    tests with the appropriate operation context.
    
    Fixes #12566.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Listeners receive `TestConnectionPreEvent`, `TestConnectionEvent`, and
    `TestConnectionFailureEvent`. Audit records identify these operations as
    `TEST_CONNECTION_CATALOG`.
    
    Custom listeners can access request parameters and the original
    exception and are responsible for redaction before logging or forwarding
    them. Existing built-in audit formatting and redaction behavior is
    unchanged.
    
    ### How was this patch tested?
    
    - `./gradlew :core:spotlessApply`
    - `./gradlew :core:test --tests
    org.apache.gravitino.listener.api.event.TestCatalogConnectionEvent
    --tests org.apache.gravitino.listener.api.event.TestCatalogEvent --tests
    org.apache.gravitino.audit.TestOperation --tests
    org.apache.gravitino.audit.v2.TestCompatibilityUtils -PskipITs
    -PskipDockerTests=true --max-workers=2 --console=plain`: 56 tests
    passed, no failures or skips.
    - Coverage includes all overloads and empty changes, request context,
    event ordering and counts, user identity, original exception
    propagation, pre-event vetoes, failing listeners, and exclusion of
    request payloads from built-in audit output.
    
    ---------
    
    Signed-off-by: Vedant Madane <[email protected]>
    Co-authored-by: mchades <[email protected]>
    Co-authored-by: Qi Yu <[email protected]>
---
 .../java/org/apache/gravitino/audit/AuditLog.java  |   7 +
 .../gravitino/audit/v2/CompatibilityUtils.java     |   1 +
 .../gravitino/listener/CatalogEventDispatcher.java |  41 +++-
 .../listener/api/event/OperationType.java          |   1 +
 .../listener/api/event/TestConnectionEvent.java    |  91 ++++++++
 .../api/event/TestConnectionFailureEvent.java      |  95 ++++++++
 .../listener/api/event/TestConnectionPreEvent.java |  91 ++++++++
 .../org/apache/gravitino/audit/TestOperation.java  |  12 +
 .../gravitino/audit/v2/TestCompatibilityUtils.java |   1 +
 .../api/event/TestCatalogConnectionEvent.java      | 258 +++++++++++++++++++++
 .../listener/api/event/TestCatalogEvent.java       |  72 ++++++
 .../server/web/rest/ExceptionHandlers.java         |   6 +-
 .../rest/TestCatalogConnectionVetoOperations.java  | 157 +++++++++++++
 13 files changed, 826 insertions(+), 7 deletions(-)

diff --git a/core/src/main/java/org/apache/gravitino/audit/AuditLog.java 
b/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
index fc6d9ac5af..3f6097b320 100644
--- a/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
+++ b/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
@@ -109,6 +109,8 @@ import 
org.apache.gravitino.listener.api.event.PurgeTableEvent;
 import org.apache.gravitino.listener.api.event.PurgeTableFailureEvent;
 import org.apache.gravitino.listener.api.event.RemovePolicyFromTagEvent;
 import org.apache.gravitino.listener.api.event.RemovePolicyFromTagFailureEvent;
+import org.apache.gravitino.listener.api.event.TestConnectionEvent;
+import org.apache.gravitino.listener.api.event.TestConnectionFailureEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.AlterSemanticModelEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.AlterSemanticModelFailureEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.CreateSemanticModelEvent;
@@ -256,6 +258,8 @@ public interface AuditLog {
 
     DISABLE_CATALOG,
 
+    TEST_CONNECTION_CATALOG,
+
     CREATE_SCHEMA,
 
     ALTER_SCHEMA,
@@ -587,6 +591,9 @@ public interface AuditLog {
       } else if (event instanceof DisableCatalogEvent
           || event instanceof DisableCatalogFailureEvent) {
         return DISABLE_CATALOG;
+      } else if (event instanceof TestConnectionEvent
+          || event instanceof TestConnectionFailureEvent) {
+        return TEST_CONNECTION_CATALOG;
       } else if (event instanceof CreateSchemaEvent || event instanceof 
CreateSchemaFailureEvent) {
         return CREATE_SCHEMA;
       } else if (event instanceof AlterSchemaEvent || event instanceof 
AlterSchemaFailureEvent) {
diff --git 
a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java 
b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
index 83ad070753..2255998d3e 100644
--- a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
+++ b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
@@ -43,6 +43,7 @@ public class CompatibilityUtils {
           .put(OperationType.LIST_CATALOG, Operation.LIST_CATALOG)
           .put(OperationType.ENABLE_CATALOG, Operation.ENABLE_CATALOG)
           .put(OperationType.DISABLE_CATALOG, Operation.DISABLE_CATALOG)
+          .put(OperationType.TEST_CONNECTION_CATALOG, 
Operation.TEST_CONNECTION_CATALOG)
           .put(OperationType.CREATE_SCHEMA, Operation.CREATE_SCHEMA)
           .put(OperationType.DROP_SCHEMA, Operation.DROP_SCHEMA)
           .put(OperationType.ALTER_SCHEMA, Operation.ALTER_SCHEMA)
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java 
b/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
index e69f1d5c0c..d2e5aa0aa9 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/CatalogEventDispatcher.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.listener;
 
 import java.util.Collections;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.CatalogChange;
 import org.apache.gravitino.NameIdentifier;
@@ -53,6 +54,9 @@ import 
org.apache.gravitino.listener.api.event.ListCatalogPreEvent;
 import org.apache.gravitino.listener.api.event.LoadCatalogEvent;
 import org.apache.gravitino.listener.api.event.LoadCatalogFailureEvent;
 import org.apache.gravitino.listener.api.event.LoadCatalogPreEvent;
+import org.apache.gravitino.listener.api.event.TestConnectionEvent;
+import org.apache.gravitino.listener.api.event.TestConnectionFailureEvent;
+import org.apache.gravitino.listener.api.event.TestConnectionPreEvent;
 import org.apache.gravitino.listener.api.info.CatalogInfo;
 import org.apache.gravitino.secret.SecretBinding;
 import org.apache.gravitino.secret.SecretReference;
@@ -218,20 +222,23 @@ public class CatalogEventDispatcher implements 
CatalogDispatcher {
       String comment,
       Map<String, String> properties)
       throws Exception {
-    // TODO(#12566): Support event dispatching for testConnection
-    dispatcher.testConnection(ident, type, provider, comment, properties);
+    CatalogInfo catalogInfo =
+        new CatalogInfo(ident.name(), type, provider, comment, properties, 
null);
+    dispatchTestConnection(
+        ident,
+        catalogInfo,
+        null,
+        () -> dispatcher.testConnection(ident, type, provider, comment, 
properties));
   }
 
   @Override
   public void testConnection(NameIdentifier ident) throws Exception {
-    // TODO(#12566): Support event dispatching for testConnection
-    dispatcher.testConnection(ident);
+    dispatchTestConnection(ident, null, null, () -> 
dispatcher.testConnection(ident));
   }
 
   @Override
   public void testConnection(NameIdentifier ident, CatalogChange... changes) 
throws Exception {
-    // TODO(#12566): Support event dispatching for testConnection
-    dispatcher.testConnection(ident, changes);
+    dispatchTestConnection(ident, null, changes, () -> 
dispatcher.testConnection(ident, changes));
   }
 
   @Override
@@ -260,4 +267,26 @@ public class CatalogEventDispatcher implements 
CatalogDispatcher {
       throw e;
     }
   }
+
+  private void dispatchTestConnection(
+      NameIdentifier ident,
+      @Nullable CatalogInfo catalogInfo,
+      @Nullable CatalogChange[] changes,
+      ConnectionTestAction action)
+      throws Exception {
+    String user = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(new TestConnectionPreEvent(user, ident, 
catalogInfo, changes));
+    try {
+      action.run();
+      eventBus.dispatchEvent(new TestConnectionEvent(user, ident, catalogInfo, 
changes));
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new TestConnectionFailureEvent(user, ident, e, 
catalogInfo, changes));
+      throw e;
+    }
+  }
+
+  @FunctionalInterface
+  private interface ConnectionTestAction {
+    void run() throws Exception;
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java 
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
index e21c70fe34..46017e4a8a 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
@@ -74,6 +74,7 @@ public enum OperationType {
   LIST_CATALOG,
   ENABLE_CATALOG,
   DISABLE_CATALOG,
+  TEST_CONNECTION_CATALOG,
 
   // Partition event
   ADD_PARTITION,
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionEvent.java
new file mode 100644
index 0000000000..949b39ea94
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionEvent.java
@@ -0,0 +1,91 @@
+/*
+ * 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.gravitino.listener.api.event;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.info.CatalogInfo;
+
+/**
+ * Represents an event that is triggered upon a successful catalog connection 
test.
+ *
+ * <p>Request parameters are available without redaction, consistent with 
other catalog events.
+ * Listeners are responsible for redacting sensitive values before forwarding 
or logging them.
+ */
+@DeveloperApi
+public final class TestConnectionEvent extends CatalogEvent {
+  @Nullable private final CatalogInfo catalogInfo;
+  @Nullable private final CatalogChange[] catalogChanges;
+
+  /**
+   * Constructs an instance of {@code TestConnectionEvent}.
+   *
+   * @param user The username of the individual who initiated the connection 
test.
+   * @param identifier The identifier of the catalog whose connection was 
tested.
+   */
+  public TestConnectionEvent(String user, NameIdentifier identifier) {
+    this(user, identifier, null, null);
+  }
+
+  /**
+   * Constructs a success event with the parameters supplied for the 
connection test.
+   *
+   * @param user The username of the individual who initiated the connection 
test.
+   * @param identifier The identifier of the catalog whose connection was 
tested.
+   * @param catalogInfo The parameters of a pre-create test, or null for an 
existing catalog.
+   * @param catalogChanges The proposed changes, or null when no changes were 
supplied.
+   */
+  public TestConnectionEvent(
+      String user,
+      NameIdentifier identifier,
+      @Nullable CatalogInfo catalogInfo,
+      @Nullable CatalogChange[] catalogChanges) {
+    super(user, identifier);
+    this.catalogInfo = catalogInfo;
+    this.catalogChanges = catalogChanges == null ? null : 
catalogChanges.clone();
+  }
+
+  /**
+   * Returns the catalog parameters supplied for a pre-create connection test.
+   *
+   * @return The catalog parameters, or null for an existing-catalog test.
+   */
+  @Nullable
+  public CatalogInfo catalogInfo() {
+    return catalogInfo;
+  }
+
+  /**
+   * Returns the proposed changes supplied for an existing-catalog connection 
test.
+   *
+   * @return The proposed changes, or null when no changes were supplied.
+   */
+  @Nullable
+  public CatalogChange[] catalogChanges() {
+    return catalogChanges;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.TEST_CONNECTION_CATALOG;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionFailureEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionFailureEvent.java
new file mode 100644
index 0000000000..893e9e768c
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionFailureEvent.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.gravitino.listener.api.event;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.info.CatalogInfo;
+
+/**
+ * Represents an event that is triggered when a catalog connection test fails.
+ *
+ * <p>Request parameters and the original exception are available without 
redaction, consistent with
+ * other catalog events. Listeners are responsible for redacting sensitive 
values before forwarding
+ * or logging them.
+ */
+@DeveloperApi
+public final class TestConnectionFailureEvent extends CatalogFailureEvent {
+  @Nullable private final CatalogInfo catalogInfo;
+  @Nullable private final CatalogChange[] catalogChanges;
+
+  /**
+   * Constructs an instance of {@code TestConnectionFailureEvent}.
+   *
+   * @param user The username of the individual who initiated the connection 
test.
+   * @param identifier The identifier of the catalog whose connection test 
failed.
+   * @param exception The original exception encountered during the connection 
test.
+   */
+  public TestConnectionFailureEvent(String user, NameIdentifier identifier, 
Exception exception) {
+    this(user, identifier, exception, null, null);
+  }
+
+  /**
+   * Constructs a failure event with the parameters supplied for the 
connection test.
+   *
+   * @param user The username of the individual who initiated the connection 
test.
+   * @param identifier The identifier of the catalog whose connection test 
failed.
+   * @param exception The original exception encountered during the connection 
test.
+   * @param catalogInfo The parameters of a pre-create test, or null for an 
existing catalog.
+   * @param catalogChanges The proposed changes, or null when no changes were 
supplied.
+   */
+  public TestConnectionFailureEvent(
+      String user,
+      NameIdentifier identifier,
+      Exception exception,
+      @Nullable CatalogInfo catalogInfo,
+      @Nullable CatalogChange[] catalogChanges) {
+    super(user, identifier, exception);
+    this.catalogInfo = catalogInfo;
+    this.catalogChanges = catalogChanges == null ? null : 
catalogChanges.clone();
+  }
+
+  /**
+   * Returns the catalog parameters supplied for a pre-create connection test.
+   *
+   * @return The catalog parameters, or null for an existing-catalog test.
+   */
+  @Nullable
+  public CatalogInfo catalogInfo() {
+    return catalogInfo;
+  }
+
+  /**
+   * Returns the proposed changes supplied for an existing-catalog connection 
test.
+   *
+   * @return The proposed changes, or null when no changes were supplied.
+   */
+  @Nullable
+  public CatalogChange[] catalogChanges() {
+    return catalogChanges;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.TEST_CONNECTION_CATALOG;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionPreEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionPreEvent.java
new file mode 100644
index 0000000000..5bca58edbc
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/TestConnectionPreEvent.java
@@ -0,0 +1,91 @@
+/*
+ * 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.gravitino.listener.api.event;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.info.CatalogInfo;
+
+/**
+ * Represents an event that is generated before a catalog connection test.
+ *
+ * <p>Request parameters are available without redaction, consistent with 
other catalog events.
+ * Listeners are responsible for redacting sensitive values before forwarding 
or logging them.
+ */
+@DeveloperApi
+public final class TestConnectionPreEvent extends CatalogPreEvent {
+  @Nullable private final CatalogInfo catalogInfo;
+  @Nullable private final CatalogChange[] catalogChanges;
+
+  /**
+   * Constructs an instance of {@code TestConnectionPreEvent}.
+   *
+   * @param user The username of the individual who initiated the connection 
test.
+   * @param identifier The identifier of the catalog whose connection is being 
tested.
+   */
+  public TestConnectionPreEvent(String user, NameIdentifier identifier) {
+    this(user, identifier, null, null);
+  }
+
+  /**
+   * Constructs a pre-event with the parameters supplied for the connection 
test.
+   *
+   * @param user The username of the individual who initiated the connection 
test.
+   * @param identifier The identifier of the catalog whose connection is being 
tested.
+   * @param catalogInfo The parameters of a pre-create test, or null for an 
existing catalog.
+   * @param catalogChanges The proposed changes, or null when no changes were 
supplied.
+   */
+  public TestConnectionPreEvent(
+      String user,
+      NameIdentifier identifier,
+      @Nullable CatalogInfo catalogInfo,
+      @Nullable CatalogChange[] catalogChanges) {
+    super(user, identifier);
+    this.catalogInfo = catalogInfo;
+    this.catalogChanges = catalogChanges == null ? null : 
catalogChanges.clone();
+  }
+
+  /**
+   * Returns the catalog parameters supplied for a pre-create connection test.
+   *
+   * @return The catalog parameters, or null for an existing-catalog test.
+   */
+  @Nullable
+  public CatalogInfo catalogInfo() {
+    return catalogInfo;
+  }
+
+  /**
+   * Returns the proposed changes supplied for an existing-catalog connection 
test.
+   *
+   * @return The proposed changes, or null when no changes were supplied.
+   */
+  @Nullable
+  public CatalogChange[] catalogChanges() {
+    return catalogChanges;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.TEST_CONNECTION_CATALOG;
+  }
+}
diff --git a/core/src/test/java/org/apache/gravitino/audit/TestOperation.java 
b/core/src/test/java/org/apache/gravitino/audit/TestOperation.java
index 243cce5fdb..12d61beedc 100644
--- a/core/src/test/java/org/apache/gravitino/audit/TestOperation.java
+++ b/core/src/test/java/org/apache/gravitino/audit/TestOperation.java
@@ -112,6 +112,8 @@ import 
org.apache.gravitino.listener.api.event.PurgePartitionFailureEvent;
 import org.apache.gravitino.listener.api.event.PurgeTableEvent;
 import org.apache.gravitino.listener.api.event.RemovePolicyFromTagEvent;
 import org.apache.gravitino.listener.api.event.RemovePolicyFromTagFailureEvent;
+import org.apache.gravitino.listener.api.event.TestConnectionEvent;
+import org.apache.gravitino.listener.api.event.TestConnectionFailureEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.AlterSemanticModelEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.AlterSemanticModelFailureEvent;
 import 
org.apache.gravitino.listener.api.event.semantic.CreateSemanticModelEvent;
@@ -677,6 +679,16 @@ public class TestOperation {
     Assertions.assertEquals(
         AuditLog.Operation.DISABLE_CATALOG,
         AuditLog.Operation.fromEvent(disableCatalogFailureEvent));
+
+    Event testConnectionEvent = new TestConnectionEvent(USER, 
catalogIdentifier);
+    Assertions.assertEquals(
+        AuditLog.Operation.TEST_CONNECTION_CATALOG,
+        AuditLog.Operation.fromEvent(testConnectionEvent));
+    Event testConnectionFailureEvent =
+        new TestConnectionFailureEvent(USER, catalogIdentifier, new 
Exception());
+    Assertions.assertEquals(
+        AuditLog.Operation.TEST_CONNECTION_CATALOG,
+        AuditLog.Operation.fromEvent(testConnectionFailureEvent));
   }
 
   @Test
diff --git 
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java 
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
index 6aa10849af..3e23ce2be9 100644
--- 
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
+++ 
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
@@ -49,6 +49,7 @@ public class TestCompatibilityUtils {
       {OperationType.LIST_CATALOG, Operation.LIST_CATALOG},
       {OperationType.ENABLE_CATALOG, Operation.ENABLE_CATALOG},
       {OperationType.DISABLE_CATALOG, Operation.DISABLE_CATALOG},
+      {OperationType.TEST_CONNECTION_CATALOG, 
Operation.TEST_CONNECTION_CATALOG},
       {OperationType.CREATE_SCHEMA, Operation.CREATE_SCHEMA},
       {OperationType.ALTER_SCHEMA, Operation.ALTER_SCHEMA},
       {OperationType.DROP_SCHEMA, Operation.DROP_SCHEMA},
diff --git 
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestCatalogConnectionEvent.java
 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestCatalogConnectionEvent.java
new file mode 100644
index 0000000000..f1fffc9375
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestCatalogConnectionEvent.java
@@ -0,0 +1,258 @@
+/*
+ * 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.gravitino.listener.api.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.CatalogChange;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.audit.JsonAuditFormatter;
+import org.apache.gravitino.audit.v2.SimpleFormatterV2;
+import org.apache.gravitino.catalog.CatalogDispatcher;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.listener.CatalogEventDispatcher;
+import org.apache.gravitino.listener.DummyEventListener;
+import org.apache.gravitino.listener.EventBus;
+import org.apache.gravitino.listener.api.info.CatalogInfo;
+import org.apache.gravitino.utils.PrincipalUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.mockito.InOrder;
+
+/** Tests the lifecycle, request context, and audit output of catalog 
connection events. */
+public class TestCatalogConnectionEvent {
+  private static final NameIdentifier IDENTIFIER = 
NameIdentifier.of("metalake", "catalog");
+  private static final String USER = "connection-test-user";
+  private static final String SECRET = "connection-test-secret";
+  private static final Map<String, String> PROPERTIES = 
ImmutableMap.of("jdbc-password", SECRET);
+  private static final CatalogChange[] CHANGES =
+      new CatalogChange[] {CatalogChange.setProperty("jdbc-password", SECRET)};
+
+  private DummyEventListener listener;
+  private CatalogDispatcher delegate;
+  private CatalogEventDispatcher dispatcher;
+
+  @BeforeEach
+  void setUp() {
+    listener = spy(new DummyEventListener());
+    delegate = mock(CatalogDispatcher.class);
+    dispatcher =
+        new CatalogEventDispatcher(new 
EventBus(Collections.singletonList(listener)), delegate);
+  }
+
+  @ParameterizedTest
+  @EnumSource(ConnectionTestMode.class)
+  void testSuccessEvents(ConnectionTestMode mode) throws Exception {
+    invokeAsUser(mode);
+
+    assertEvents(mode, TestConnectionEvent.class, OperationStatus.SUCCESS);
+  }
+
+  @ParameterizedTest
+  @EnumSource(ConnectionTestMode.class)
+  void testFailureEventsPreserveOriginalException(ConnectionTestMode mode) 
throws Exception {
+    Exception original = new Exception("jdbc-password=" + SECRET, new 
Exception("cause=" + SECRET));
+    original.addSuppressed(new Exception("suppressed=" + SECRET));
+    invoke(mode, doThrow(original).when(delegate));
+
+    Assertions.assertSame(
+        original, Assertions.assertThrowsExactly(Exception.class, () -> 
invokeAsUser(mode)));
+
+    TestConnectionFailureEvent event =
+        (TestConnectionFailureEvent)
+            assertEvents(mode, TestConnectionFailureEvent.class, 
OperationStatus.FAILURE);
+    Assertions.assertSame(original, event.exception());
+  }
+
+  @ParameterizedTest
+  @EnumSource(ConnectionTestMode.class)
+  void testPreEventVetoPreventsConnectionTest(ConnectionTestMode mode) {
+    ForbiddenException forbidden = new ForbiddenException("denied");
+    
doThrow(forbidden).when(listener).onPreEvent(any(TestConnectionPreEvent.class));
+
+    Assertions.assertSame(
+        forbidden,
+        Assertions.assertThrowsExactly(ForbiddenException.class, () -> 
invokeAsUser(mode)));
+
+    verify(listener).onPreEvent(any(TestConnectionPreEvent.class));
+    verify(listener, never()).onPostEvent(any(Event.class));
+    verifyNoInteractions(delegate);
+  }
+
+  @ParameterizedTest
+  @EnumSource(ConnectionTestMode.class)
+  void testFailureListenerDoesNotMaskOriginalException(ConnectionTestMode 
mode) throws Exception {
+    Exception original = new Exception("jdbc-password=" + SECRET);
+    invoke(mode, doThrow(original).when(delegate));
+    doThrow(new IllegalStateException("listener failed"))
+        .when(listener)
+        .onPostEvent(any(TestConnectionFailureEvent.class));
+
+    Assertions.assertSame(
+        original, Assertions.assertThrowsExactly(Exception.class, () -> 
invokeAsUser(mode)));
+
+    verify(listener).onPreEvent(any(TestConnectionPreEvent.class));
+    verify(listener).onPostEvent(any(TestConnectionFailureEvent.class));
+    verify(listener, never()).onPostEvent(any(TestConnectionEvent.class));
+    invoke(mode, verify(delegate));
+    verifyNoMoreInteractions(delegate);
+  }
+
+  @Test
+  void testIdentifierOnlyConstructors() {
+    TestConnectionPreEvent preEvent = new TestConnectionPreEvent(USER, 
IDENTIFIER);
+    assertRequestContext(
+        ConnectionTestMode.STORED, preEvent.catalogInfo(), 
preEvent.catalogChanges());
+    TestConnectionEvent event = new TestConnectionEvent(USER, IDENTIFIER);
+    assertRequestContext(ConnectionTestMode.STORED, event.catalogInfo(), 
event.catalogChanges());
+    Exception original = new Exception("connection failed");
+    TestConnectionFailureEvent failureEvent =
+        new TestConnectionFailureEvent(USER, IDENTIFIER, original);
+    assertRequestContext(
+        ConnectionTestMode.STORED, failureEvent.catalogInfo(), 
failureEvent.catalogChanges());
+    Assertions.assertSame(original, failureEvent.exception());
+  }
+
+  private Event assertEvents(
+      ConnectionTestMode mode, Class<? extends Event> eventClass, 
OperationStatus status)
+      throws Exception {
+    Assertions.assertEquals(1, listener.getPreEvents().size());
+    Assertions.assertEquals(1, listener.getPostEvents().size());
+    TestConnectionPreEvent preEvent =
+        Assertions.assertInstanceOf(TestConnectionPreEvent.class, 
listener.popPreEvent());
+    assertEventFields(preEvent, OperationStatus.UNPROCESSED);
+    assertRequestContext(mode, preEvent.catalogInfo(), 
preEvent.catalogChanges());
+    Event event = listener.popPostEvent();
+    Assertions.assertEquals(eventClass, event.getClass());
+    assertEventFields(event, status);
+    if (event instanceof TestConnectionEvent) {
+      TestConnectionEvent successEvent = (TestConnectionEvent) event;
+      assertRequestContext(mode, successEvent.catalogInfo(), 
successEvent.catalogChanges());
+    } else {
+      TestConnectionFailureEvent failureEvent =
+          Assertions.assertInstanceOf(TestConnectionFailureEvent.class, event);
+      assertRequestContext(mode, failureEvent.catalogInfo(), 
failureEvent.catalogChanges());
+    }
+    assertAuditOutput(new JsonAuditFormatter().format(event).toString());
+    assertAuditOutput(new SimpleFormatterV2().format(event).toString());
+
+    InOrder ordered = inOrder(listener, delegate);
+    ordered.verify(listener).onPreEvent(preEvent);
+    invoke(mode, ordered.verify(delegate));
+    ordered.verify(listener).onPostEvent(event);
+    verifyNoMoreInteractions(delegate);
+    return event;
+  }
+
+  private void assertEventFields(BaseEvent event, OperationStatus status) {
+    Assertions.assertEquals(USER, event.user());
+    Assertions.assertEquals(IDENTIFIER, event.identifier());
+    Assertions.assertEquals(OperationType.TEST_CONNECTION_CATALOG, 
event.operationType());
+    Assertions.assertEquals(status, event.operationStatus());
+    Assertions.assertTrue(event.customInfo().isEmpty());
+  }
+
+  private void assertRequestContext(
+      ConnectionTestMode mode,
+      @Nullable CatalogInfo catalogInfo,
+      @Nullable CatalogChange[] catalogChanges) {
+    if (mode == ConnectionTestMode.PRE_CREATE) {
+      Assertions.assertNotNull(catalogInfo);
+      Assertions.assertEquals(IDENTIFIER.name(), catalogInfo.name());
+      Assertions.assertEquals(Catalog.Type.RELATIONAL, catalogInfo.type());
+      Assertions.assertEquals("jdbc-mysql", catalogInfo.provider());
+      Assertions.assertEquals("comment", catalogInfo.comment());
+      Assertions.assertEquals(PROPERTIES, catalogInfo.properties());
+      Assertions.assertNull(catalogInfo.auditInfo());
+    } else {
+      Assertions.assertNull(catalogInfo);
+    }
+
+    if (mode == ConnectionTestMode.PROPOSED_CHANGES) {
+      Assertions.assertArrayEquals(CHANGES, catalogChanges);
+      Assertions.assertNotSame(CHANGES, catalogChanges);
+    } else if (mode == ConnectionTestMode.EMPTY_CHANGES) {
+      Assertions.assertArrayEquals(new CatalogChange[0], catalogChanges);
+    } else {
+      Assertions.assertNull(catalogChanges);
+    }
+  }
+
+  private void assertAuditOutput(String auditLog) {
+    Assertions.assertTrue(auditLog.contains(USER));
+    Assertions.assertTrue(auditLog.contains(IDENTIFIER.toString()));
+    
Assertions.assertTrue(auditLog.contains(OperationType.TEST_CONNECTION_CATALOG.name()));
+    Assertions.assertFalse(auditLog.contains(SECRET));
+    Assertions.assertFalse(auditLog.contains("jdbc-password"));
+  }
+
+  private void invokeAsUser(ConnectionTestMode mode) throws Exception {
+    PrincipalUtils.doAs(
+        new UserPrincipal(USER),
+        () -> {
+          invoke(mode, dispatcher);
+          return null;
+        });
+  }
+
+  private void invoke(ConnectionTestMode mode, CatalogDispatcher target) 
throws Exception {
+    switch (mode) {
+      case PRE_CREATE:
+        target.testConnection(
+            IDENTIFIER, Catalog.Type.RELATIONAL, "jdbc-mysql", "comment", 
PROPERTIES);
+        break;
+      case STORED:
+        target.testConnection(IDENTIFIER);
+        break;
+      case PROPOSED_CHANGES:
+        target.testConnection(IDENTIFIER, CHANGES);
+        break;
+      case EMPTY_CHANGES:
+        target.testConnection(IDENTIFIER, new CatalogChange[0]);
+        break;
+      default:
+        throw new AssertionError("Unexpected connection test mode: " + mode);
+    }
+  }
+
+  private enum ConnectionTestMode {
+    PRE_CREATE,
+    STORED,
+    PROPOSED_CHANGES,
+    EMPTY_CHANGES
+  }
+}
diff --git 
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestCatalogEvent.java
 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestCatalogEvent.java
index 51f26585e5..e244a49bd6 100644
--- 
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestCatalogEvent.java
+++ 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestCatalogEvent.java
@@ -40,6 +40,7 @@ import org.apache.gravitino.listener.EventBus;
 import org.apache.gravitino.listener.api.info.CatalogInfo;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.TestInstance.Lifecycle;
@@ -62,6 +63,11 @@ public class TestCatalogEvent {
     this.failureDispatcher = new CatalogEventDispatcher(eventBus, 
catalogExceptionDispatcher);
   }
 
+  @BeforeEach
+  void clearEvents() {
+    dummyEventListener.clear();
+  }
+
   @Test
   void testCreateCatalogEvent() {
     NameIdentifier identifier = NameIdentifier.of("metalake", catalog.name());
@@ -357,6 +363,72 @@ public class TestCatalogEvent {
     Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
   }
 
+  @Test
+  void testTestConnectionEvent() throws Exception {
+    NameIdentifier identifier = NameIdentifier.of("metalake", catalog.name());
+    dispatcher.testConnection(
+        identifier, catalog.type(), catalog.provider(), catalog.comment(), 
catalog.properties());
+    Event event = dummyEventListener.popPostEvent();
+    Assertions.assertEquals(identifier, event.identifier());
+    Assertions.assertEquals(TestConnectionEvent.class, event.getClass());
+    Assertions.assertEquals(OperationType.TEST_CONNECTION_CATALOG, 
event.operationType());
+    Assertions.assertEquals(OperationStatus.SUCCESS, event.operationStatus());
+
+    PreEvent preEvent = dummyEventListener.popPreEvent();
+    Assertions.assertEquals(identifier, preEvent.identifier());
+    Assertions.assertEquals(TestConnectionPreEvent.class, preEvent.getClass());
+    Assertions.assertEquals(OperationType.TEST_CONNECTION_CATALOG, 
preEvent.operationType());
+    Assertions.assertEquals(OperationStatus.UNPROCESSED, 
preEvent.operationStatus());
+  }
+
+  @Test
+  void testTestConnectionFailureEvent() {
+    NameIdentifier identifier = NameIdentifier.of("metalake", "fail");
+    Assertions.assertThrowsExactly(
+        GravitinoRuntimeException.class,
+        () ->
+            failureDispatcher.testConnection(
+                identifier,
+                catalog.type(),
+                catalog.provider(),
+                catalog.comment(),
+                catalog.properties()));
+    Event event = dummyEventListener.popPostEvent();
+    Assertions.assertEquals(identifier, event.identifier());
+    Assertions.assertEquals(TestConnectionFailureEvent.class, 
event.getClass());
+    Assertions.assertEquals(OperationType.TEST_CONNECTION_CATALOG, 
event.operationType());
+    Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
+  }
+
+  @Test
+  void testTestConnectionExistingCatalogEvent() throws Exception {
+    NameIdentifier identifier = NameIdentifier.of("metalake", catalog.name());
+    dispatcher.testConnection(identifier);
+    Event event = dummyEventListener.popPostEvent();
+    Assertions.assertEquals(identifier, event.identifier());
+    Assertions.assertEquals(TestConnectionEvent.class, event.getClass());
+    Assertions.assertEquals(OperationType.TEST_CONNECTION_CATALOG, 
event.operationType());
+    Assertions.assertEquals(OperationStatus.SUCCESS, event.operationStatus());
+
+    PreEvent preEvent = dummyEventListener.popPreEvent();
+    Assertions.assertEquals(identifier, preEvent.identifier());
+    Assertions.assertEquals(TestConnectionPreEvent.class, preEvent.getClass());
+    Assertions.assertEquals(OperationType.TEST_CONNECTION_CATALOG, 
preEvent.operationType());
+    Assertions.assertEquals(OperationStatus.UNPROCESSED, 
preEvent.operationStatus());
+  }
+
+  @Test
+  void testTestConnectionExistingCatalogFailureEvent() {
+    NameIdentifier identifier = NameIdentifier.of("metalake", "fail");
+    Assertions.assertThrowsExactly(
+        GravitinoRuntimeException.class, () -> 
failureDispatcher.testConnection(identifier));
+    Event event = dummyEventListener.popPostEvent();
+    Assertions.assertEquals(identifier, event.identifier());
+    Assertions.assertEquals(TestConnectionFailureEvent.class, 
event.getClass());
+    Assertions.assertEquals(OperationType.TEST_CONNECTION_CATALOG, 
event.operationType());
+    Assertions.assertEquals(OperationStatus.FAILURE, event.operationStatus());
+  }
+
   private void checkCatalogInfo(CatalogInfo catalogInfo, Catalog catalog) {
     Assertions.assertEquals(catalog.name(), catalogInfo.name());
     Assertions.assertEquals(catalog.type(), catalogInfo.type());
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
index 34990dc7d3..a9eeaad49f 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/ExceptionHandlers.java
@@ -194,7 +194,8 @@ public class ExceptionHandlers {
    * Handles an existing catalog connection test failure without exposing its 
stack trace.
    *
    * @param e the connection test failure
-   * @return an HTTP 200 response containing the application error
+   * @return an HTTP 403 response for forbidden operations, an HTTP 200 
response for known
+   *     connection test failures, or an HTTP 500 response for unexpected 
errors
    */
   public static Response 
handleExistingCatalogTestConnectionException(Exception e) {
     return handleTestConnectionException(e, false);
@@ -242,6 +243,9 @@ public class ExceptionHandlers {
     } else if (e instanceof NotInUseException) {
       response = ErrorResponse.notInUse(e.getClass().getSimpleName(), 
e.getMessage(), throwable);
 
+    } else if (e instanceof ForbiddenException) {
+      return Utils.forbidden(e.getMessage(), throwable);
+
     } else {
       return Utils.internalError(e.getMessage(), throwable);
     }
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogConnectionVetoOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogConnectionVetoOperations.java
new file mode 100644
index 0000000000..850c87d6f8
--- /dev/null
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestCatalogConnectionVetoOperations.java
@@ -0,0 +1,157 @@
+/*
+ * 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.gravitino.server.web.rest;
+
+import static org.mockito.ArgumentMatchers.any;
+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 java.io.IOException;
+import java.util.Collections;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.catalog.CatalogDispatcher;
+import org.apache.gravitino.dto.requests.CatalogCreateRequest;
+import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
+import org.apache.gravitino.dto.requests.CatalogUpdatesRequest;
+import org.apache.gravitino.dto.responses.ErrorConstants;
+import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.listener.CatalogEventDispatcher;
+import org.apache.gravitino.listener.EventBus;
+import org.apache.gravitino.listener.api.EventListenerPlugin;
+import org.apache.gravitino.listener.api.event.Event;
+import org.apache.gravitino.listener.api.event.TestConnectionPreEvent;
+import org.apache.gravitino.rest.RESTUtils;
+import org.glassfish.hk2.utilities.binding.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests HTTP responses and delegate isolation when a connection-test 
pre-listener vetoes a request.
+ */
+public class TestCatalogConnectionVetoOperations extends BaseOperationsTest {
+  private static final String CATALOGS_PATH = "/metalakes/metalake1/catalogs/";
+  private static final String DENIED_MESSAGE = "Connection test denied by 
pre-listener";
+
+  private final CatalogDispatcher delegate = mock(CatalogDispatcher.class);
+  private final EventListenerPlugin listener = mock(EventListenerPlugin.class);
+
+  /** {@inheritDoc} */
+  @Override
+  protected Application configure() {
+    try {
+      forceSet(
+          TestProperties.CONTAINER_PORT, 
String.valueOf(RESTUtils.findAvailablePort(2000, 3000)));
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+
+    ResourceConfig resourceConfig = new 
ResourceConfig(CatalogOperations.class);
+    resourceConfig.register(
+        new AbstractBinder() {
+          /** {@inheritDoc} */
+          @Override
+          protected void configure() {
+            CatalogEventDispatcher dispatcher =
+                new CatalogEventDispatcher(
+                    new EventBus(Collections.singletonList(listener)), 
delegate);
+            bind(dispatcher).to(CatalogDispatcher.class).ranked(2);
+            
bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class);
+          }
+        });
+    return resourceConfig;
+  }
+
+  @Test
+  void testPreCreateConnectionVeto() {
+    CatalogCreateRequest request =
+        new CatalogCreateRequest(
+            "catalog1", Catalog.Type.RELATIONAL, "test", "comment", 
Collections.emptyMap());
+    assertPreEventVeto("testConnection", Entity.json(request), true);
+  }
+
+  @Test
+  void testStoredConnectionVeto() {
+    assertPreEventVeto("catalog1/testConnection", null, false);
+  }
+
+  @Test
+  void testProposedChangesConnectionVeto() {
+    CatalogUpdatesRequest request =
+        new CatalogUpdatesRequest(
+            Collections.singletonList(
+                new CatalogUpdateRequest.SetCatalogPropertyRequest("key", 
"value")));
+    assertPreEventVeto("catalog1/testConnection", Entity.json(request), false);
+  }
+
+  @Test
+  void testEmptyChangesConnectionVeto() {
+    assertPreEventVeto(
+        "catalog1/testConnection",
+        Entity.json(new CatalogUpdatesRequest(Collections.emptyList())),
+        false);
+  }
+
+  private void assertPreEventVeto(String path, @Nullable Entity<?> request, 
boolean includeStack) {
+    doThrow(new ForbiddenException(DENIED_MESSAGE))
+        .when(listener)
+        .onPreEvent(any(TestConnectionPreEvent.class));
+
+    try (Response response =
+        target(CATALOGS_PATH + path)
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(request)) {
+      ErrorResponse error = response.readEntity(ErrorResponse.class);
+      Assertions.assertEquals(
+          Response.Status.FORBIDDEN.getStatusCode(), response.getStatus(), 
error.getMessage());
+      Assertions.assertEquals(ErrorConstants.FORBIDDEN_CODE, error.getCode());
+      Assertions.assertEquals(ForbiddenException.class.getSimpleName(), 
error.getType());
+      Assertions.assertEquals(DENIED_MESSAGE, error.getMessage());
+      if (includeStack) {
+        Assertions.assertNotNull(error.getStack());
+      } else {
+        Assertions.assertNull(error.getStack());
+      }
+    }
+
+    verify(listener).onPreEvent(any(TestConnectionPreEvent.class));
+    verify(listener, never()).onPostEvent(any(Event.class));
+    verifyNoInteractions(delegate);
+  }
+
+  private static class MockServletRequestFactory extends 
ServletRequestFactoryBase {
+    /** {@inheritDoc} */
+    @Override
+    public HttpServletRequest get() {
+      return mock(HttpServletRequest.class);
+    }
+  }
+}

Reply via email to