This is an automated email from the ASF dual-hosted git repository.
roryqi 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 66305b70fd [MINOR] test(IRC): Add unit tests for
IcebergTableHookDispatcher (#9937)
66305b70fd is described below
commit 66305b70fd486053931dcdc20b925cd4a8c37f3d
Author: Bharath Krishna <[email protected]>
AuthorDate: Tue Feb 10 20:05:42 2026 +0530
[MINOR] test(IRC): Add unit tests for IcebergTableHookDispatcher (#9937)
### What changes were proposed in this pull request?
Add unit tests for IcebergTableHookDispatcher
### Why are the changes needed?
Missing unit tests for this class
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
Unit tests
---
.../dispatcher/TestIcebergTableHookDispatcher.java | 279 +++++++++++++++++++++
1 file changed, 279 insertions(+)
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
new file mode 100644
index 0000000000..7857fb2f01
--- /dev/null
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergTableHookDispatcher.java
@@ -0,0 +1,279 @@
+/*
+ * 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.iceberg.service.dispatcher;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.Owner;
+import org.apache.gravitino.authorization.OwnerDispatcher;
+import org.apache.gravitino.catalog.TableDispatcher;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.iceberg.common.utils.IcebergIdentifierUtils;
+import
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
+import org.apache.gravitino.iceberg.service.provider.IcebergConfigProvider;
+import org.apache.gravitino.listener.api.event.IcebergRequestContext;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.TableEntity;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.apache.iceberg.rest.requests.RenameTableRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.rest.responses.LoadTableResponse;
+import org.apache.iceberg.types.Types.NestedField;
+import org.apache.iceberg.types.Types.StringType;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+public class TestIcebergTableHookDispatcher {
+
+ private static final String TEST_METALAKE = "test_metalake";
+ private static final String TEST_CATALOG = "test_catalog";
+ private static final String TEST_USER = "test_user";
+ private static final Schema TABLE_SCHEMA =
+ new Schema(NestedField.required(1, "test_field", StringType.get()));
+
+ private IcebergTableHookDispatcher hookDispatcher;
+ private IcebergTableOperationDispatcher mockDispatcher;
+ private EntityStore mockEntityStore;
+ private TableDispatcher mockTableDispatcher;
+ private OwnerDispatcher mockOwnerDispatcher;
+ private IcebergRequestContext mockContext;
+
+ @BeforeEach
+ public void setUp() throws IllegalAccessException {
+ // Mock the underlying dispatcher
+ mockDispatcher = mock(IcebergTableOperationDispatcher.class);
+
+ // Mock GravitinoEnv components
+ mockEntityStore = mock(EntityStore.class);
+ mockTableDispatcher = mock(TableDispatcher.class);
+ mockOwnerDispatcher = mock(OwnerDispatcher.class);
+
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore",
mockEntityStore, true);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "tableDispatcher",
mockTableDispatcher, true);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "ownerDispatcher",
mockOwnerDispatcher, true);
+
+ // Create mock IcebergRESTServerContext
+ IcebergConfigProvider mockConfigProvider =
mock(IcebergConfigProvider.class);
+ when(mockConfigProvider.getMetalakeName()).thenReturn(TEST_METALAKE);
+ when(mockConfigProvider.getDefaultCatalogName()).thenReturn(TEST_CATALOG);
+ IcebergRESTServerContext.create(mockConfigProvider, false, false);
+
+ // Create hook dispatcher
+ hookDispatcher = new IcebergTableHookDispatcher(mockDispatcher);
+
+ // Mock request context
+ mockContext = mock(IcebergRequestContext.class);
+ when(mockContext.catalogName()).thenReturn(TEST_CATALOG);
+ when(mockContext.userName()).thenReturn(TEST_USER);
+ }
+
+ @AfterEach
+ public void tearDown() throws IllegalAccessException {
+ // Clean up GravitinoEnv
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", null,
true);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "tableDispatcher", null,
true);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "ownerDispatcher", null,
true);
+
+ // Reset IcebergRESTServerContext singleton
+ Class<?> holderClass =
+ Arrays.stream(IcebergRESTServerContext.class.getDeclaredClasses())
+ .filter(c -> c.getSimpleName().equals("InstanceHolder"))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("InstanceHolder class not
found"));
+ FieldUtils.writeStaticField(holderClass, "INSTANCE", null, true);
+ }
+
+ @Test
+ public void testCreateTableCallsImportAndOwnership() {
+ Namespace namespace = Namespace.of("test_schema");
+ CreateTableRequest request =
+
CreateTableRequest.builder().withName("test_table").withSchema(TABLE_SCHEMA).build();
+
+ LoadTableResponse mockResponse = mock(LoadTableResponse.class);
+ when(mockDispatcher.createTable(mockContext, namespace,
request)).thenReturn(mockResponse);
+
+ LoadTableResponse result = hookDispatcher.createTable(mockContext,
namespace, request);
+
+ Assertions.assertEquals(mockResponse, result);
+ verify(mockDispatcher).createTable(mockContext, namespace, request);
+
+ // Verify table import was called
+ NameIdentifier expectedIdentifier =
+ IcebergIdentifierUtils.toGravitinoTableIdentifier(
+ TEST_METALAKE, TEST_CATALOG, TableIdentifier.of(namespace,
"test_table"));
+ verify(mockTableDispatcher).loadTable(expectedIdentifier);
+
+ // Verify ownership was set
+ ArgumentCaptor<String> userCaptor = ArgumentCaptor.forClass(String.class);
+ verify(mockOwnerDispatcher)
+ .setOwner(eq(TEST_METALAKE), any(), userCaptor.capture(),
eq(Owner.Type.USER));
+ Assertions.assertEquals(TEST_USER, userCaptor.getValue());
+ }
+
+ @Test
+ public void testDropTableDeletesEntity() throws IOException {
+ TableIdentifier tableId = TableIdentifier.of("test_schema", "test_table");
+
+ hookDispatcher.dropTable(mockContext, tableId, false);
+
+ verify(mockDispatcher).dropTable(mockContext, tableId, false);
+
+ NameIdentifier expectedIdentifier =
+ IcebergIdentifierUtils.toGravitinoTableIdentifier(TEST_METALAKE,
TEST_CATALOG, tableId);
+ verify(mockEntityStore).delete(expectedIdentifier,
Entity.EntityType.TABLE);
+ }
+
+ @Test
+ public void testDropTableIgnoresNoSuchEntityException() throws IOException {
+ TableIdentifier tableId = TableIdentifier.of("test_schema", "test_table");
+
+ NameIdentifier expectedIdentifier =
+ IcebergIdentifierUtils.toGravitinoTableIdentifier(TEST_METALAKE,
TEST_CATALOG, tableId);
+ doThrow(new NoSuchEntityException("Table not found"))
+ .when(mockEntityStore)
+ .delete(expectedIdentifier, Entity.EntityType.TABLE);
+
+ // Should not throw exception
+ Assertions.assertDoesNotThrow(() -> hookDispatcher.dropTable(mockContext,
tableId, false));
+
+ verify(mockDispatcher).dropTable(mockContext, tableId, false);
+ }
+
+ @Test
+ public void testDropTableThrowsRuntimeExceptionOnIOException() throws
IOException {
+ TableIdentifier tableId = TableIdentifier.of("test_schema", "test_table");
+
+ doThrow(new IOException("IO error")).when(mockEntityStore).delete(any(),
any());
+
+ RuntimeException exception =
+ Assertions.assertThrows(
+ RuntimeException.class, () ->
hookDispatcher.dropTable(mockContext, tableId, false));
+
+ Assertions.assertTrue(exception.getMessage().contains("io exception when
deleting table"));
+ verify(mockDispatcher).dropTable(mockContext, tableId, false);
+ }
+
+ @Test
+ public void testRenameTableUpdatesEntity() throws IOException {
+ TableIdentifier source = TableIdentifier.of("schema1", "old_table");
+ TableIdentifier dest = TableIdentifier.of("schema2", "new_table");
+ RenameTableRequest request =
+
RenameTableRequest.builder().withSource(source).withDestination(dest).build();
+
+ TableEntity mockTableEntity = mock(TableEntity.class);
+ when(mockTableEntity.id()).thenReturn(1L);
+ when(mockTableEntity.columns()).thenReturn(Collections.emptyList());
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("original_creator").withCreateTime(Instant.now()).build();
+ when(mockTableEntity.auditInfo()).thenReturn(auditInfo);
+
+ when(mockEntityStore.update(any(), eq(TableEntity.class),
eq(Entity.EntityType.TABLE), any()))
+ .thenReturn(mockTableEntity);
+
+ hookDispatcher.renameTable(mockContext, request);
+
+ verify(mockDispatcher).renameTable(mockContext, request);
+
+ NameIdentifier sourceIdentifier =
+ IcebergIdentifierUtils.toGravitinoTableIdentifier(TEST_METALAKE,
TEST_CATALOG, source);
+ verify(mockEntityStore)
+ .update(eq(sourceIdentifier), eq(TableEntity.class),
eq(Entity.EntityType.TABLE), any());
+ }
+
+ @Test
+ public void testRenameTableIgnoresNoSuchEntityException() throws IOException
{
+ TableIdentifier source = TableIdentifier.of("schema1", "old_table");
+ TableIdentifier dest = TableIdentifier.of("schema2", "new_table");
+ RenameTableRequest request =
+
RenameTableRequest.builder().withSource(source).withDestination(dest).build();
+
+ doThrow(new NoSuchEntityException("Entity not found"))
+ .when(mockEntityStore)
+ .update(any(), any(), any(), any());
+
+ // Should not throw exception
+ Assertions.assertDoesNotThrow(() ->
hookDispatcher.renameTable(mockContext, request));
+
+ verify(mockDispatcher).renameTable(mockContext, request);
+ }
+
+ @Test
+ public void testRenameTableThrowsRuntimeExceptionOnIOException() throws
IOException {
+ TableIdentifier source = TableIdentifier.of("schema1", "old_table");
+ TableIdentifier dest = TableIdentifier.of("schema2", "new_table");
+ RenameTableRequest request =
+
RenameTableRequest.builder().withSource(source).withDestination(dest).build();
+
+ doThrow(new IOException("IO error")).when(mockEntityStore).update(any(),
any(), any(), any());
+
+ RuntimeException exception =
+ Assertions.assertThrows(
+ RuntimeException.class, () ->
hookDispatcher.renameTable(mockContext, request));
+
+ Assertions.assertTrue(exception.getMessage().contains("io exception when
renaming table"));
+ verify(mockDispatcher).renameTable(mockContext, request);
+ }
+
+ @Test
+ public void testUpdateTablePassesThrough() {
+ TableIdentifier tableId = TableIdentifier.of("test_schema", "test_table");
+ UpdateTableRequest request = mock(UpdateTableRequest.class);
+ LoadTableResponse mockResponse = mock(LoadTableResponse.class);
+
+ when(mockDispatcher.updateTable(mockContext, tableId,
request)).thenReturn(mockResponse);
+
+ LoadTableResponse result = hookDispatcher.updateTable(mockContext,
tableId, request);
+
+ Assertions.assertEquals(mockResponse, result);
+ verify(mockDispatcher).updateTable(mockContext, tableId, request);
+ }
+
+ @Test
+ public void testLoadTablePassesThrough() {
+ TableIdentifier tableId = TableIdentifier.of("test_schema", "test_table");
+ LoadTableResponse mockResponse = mock(LoadTableResponse.class);
+
+ when(mockDispatcher.loadTable(mockContext,
tableId)).thenReturn(mockResponse);
+
+ LoadTableResponse result = hookDispatcher.loadTable(mockContext, tableId);
+
+ Assertions.assertEquals(mockResponse, result);
+ verify(mockDispatcher).loadTable(mockContext, tableId);
+ }
+}