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);
+ }
+ }
+}