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

yuqi1129 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 5a0d21ed21 [#10910] feat(catalog-glue): Add integration tests for glue 
catalog (#10917)
5a0d21ed21 is described below

commit 5a0d21ed2136a4206f1892a3e142e9748eb4a714
Author: Yuhui <[email protected]>
AuthorDate: Fri May 8 15:55:35 2026 +0800

    [#10910] feat(catalog-glue): Add integration tests for glue catalog (#10917)
    
    ### What changes were proposed in this pull request?
    
    Add integration test coverage for table-level features in catalog-glue,
    including Hive table distribution/sort/comment/types and Iceberg table
    operations.
    
    ### Why are the changes needed?
    
    Verify table-level features (Hive distribution, sort columns, comments,
    various types, Iceberg operations) work correctly with the AWS Glue
    backend.
    
    Fix: #10910
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Unit tests and integration tests.
---
 catalogs/catalog-glue/build.gradle.kts             |   3 +
 .../apache/gravitino/catalog/glue/GlueCatalog.java |   3 -
 ...GlueSchemaTest.java => GlueSchemaTestBase.java} |   2 +-
 ...ctGlueTableTest.java => GlueTableTestBase.java} |   2 +-
 ...chema.java => TestAwsGlueSchemaConversion.java} |   4 +-
 ...eTable.java => TestAwsGlueTableConversion.java} |   4 +-
 .../glue/TestGlueCatalogSchemaOperations.java      | 314 ++++++++++
 ...ma.java => TestGlueCatalogTableOperations.java} | 279 +--------
 ...tions.java => TestGluePartitionOperations.java} |   2 +-
 ...ueSchema.java => TestGlueSchemaConversion.java} |   4 +-
 ...GlueTable.java => TestGlueTableConversion.java} |   4 +-
 .../integration/test/AbstractGlueCatalogIT.java    | 661 +++++++++++++++++++++
 .../glue/integration/test/AwsGlueCatalogIT.java    |  57 ++
 .../glue/integration/test/MotoGlueCatalogIT.java   |  80 +++
 .../test/container/GravitinoMotoContainer.java     |  74 +++
 15 files changed, 1215 insertions(+), 278 deletions(-)

diff --git a/catalogs/catalog-glue/build.gradle.kts 
b/catalogs/catalog-glue/build.gradle.kts
index b7787263c5..b37ae580c6 100644
--- a/catalogs/catalog-glue/build.gradle.kts
+++ b/catalogs/catalog-glue/build.gradle.kts
@@ -52,6 +52,9 @@ dependencies {
   testImplementation(libs.slf4j.api)
 
   testRuntimeOnly(libs.junit.jupiter.engine)
+
+  testImplementation(libs.testcontainers)
+  testImplementation(project(":integration-test-common", "testArtifacts"))
 }
 
 tasks {
diff --git 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalog.java
 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalog.java
index f016121cfa..471bfccbc5 100644
--- 
a/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalog.java
+++ 
b/catalogs/catalog-glue/src/main/java/org/apache/gravitino/catalog/glue/GlueCatalog.java
@@ -32,7 +32,6 @@ import org.apache.gravitino.connector.capability.Capability;
  */
 public class GlueCatalog extends BaseCatalog<GlueCatalog> {
 
-  // TODO PR-02: replace stubs with real implementations
   static final GlueCatalogPropertiesMetadata CATALOG_PROPERTIES_METADATA =
       new GlueCatalogPropertiesMetadata();
 
@@ -60,13 +59,11 @@ public class GlueCatalog extends BaseCatalog<GlueCatalog> {
    */
   @Override
   protected CatalogOperations newOps(Map<String, String> config) {
-    // TODO PR-04: initialize GlueClient and wire up real operations
     return new GlueCatalogOperations();
   }
 
   @Override
   public Capability newCapability() {
-    // TODO PR-02: implement GlueCatalogCapability
     return new GlueCatalogCapability();
   }
 
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/AbstractGlueSchemaTest.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/GlueSchemaTestBase.java
similarity index 99%
rename from 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/AbstractGlueSchemaTest.java
rename to 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/GlueSchemaTestBase.java
index 6e29b2df6f..198a5a946f 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/AbstractGlueSchemaTest.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/GlueSchemaTestBase.java
@@ -34,7 +34,7 @@ import software.amazon.awssdk.services.glue.model.Database;
  * <p>Subclasses provide a {@link Database} object however they like (SDK 
builder, real AWS API,
  * etc.). The test scenarios are defined once here and shared across all 
implementations.
  */
-abstract class AbstractGlueSchemaTest {
+abstract class GlueSchemaTestBase {
 
   /**
    * Returns a Glue {@link Database} with the given fields. Subclasses may 
create this via the SDK
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/AbstractGlueTableTest.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/GlueTableTestBase.java
similarity index 99%
rename from 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/AbstractGlueTableTest.java
rename to 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/GlueTableTestBase.java
index 22f3d6c130..f51262f745 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/AbstractGlueTableTest.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/GlueTableTestBase.java
@@ -37,7 +37,7 @@ import software.amazon.awssdk.services.glue.model.Table;
  * <p>Subclasses supply the {@link Table} object — either via SDK builder 
(synthetic) or via the
  * real Glue API — while the test scenarios are defined once here.
  */
-abstract class AbstractGlueTableTest {
+abstract class GlueTableTestBase {
 
   private static final GlueTypeConverter TYPE_CONVERTER = new 
GlueTypeConverter();
 
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueSchema.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueSchemaConversion.java
similarity index 96%
rename from 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueSchema.java
rename to 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueSchemaConversion.java
index 4e07d6b35b..34a6040339 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueSchema.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueSchemaConversion.java
@@ -32,7 +32,7 @@ import 
software.amazon.awssdk.services.glue.model.GetDatabaseRequest;
 import software.amazon.awssdk.services.glue.model.GlueException;
 
 /**
- * Runs {@link AbstractGlueSchemaTest} scenarios against a real AWS Glue 
endpoint.
+ * Runs {@link GlueSchemaTestBase} scenarios against a real AWS Glue endpoint.
  *
  * <p>This test is <b>skipped by default</b> and only runs when {@code 
AWS_ACCESS_KEY_ID} is set. To
  * run it, set the following environment variables:
@@ -49,7 +49,7 @@ import 
software.amazon.awssdk.services.glue.model.GlueException;
  * deleted in {@link #cleanup} regardless of test outcome.
  */
 @EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = ".+")
-class TestAwsGlueSchema extends AbstractGlueSchemaTest {
+class TestAwsGlueSchemaConversion extends GlueSchemaTestBase {
 
   private static GlueClient glueClient;
   private static String catalogId;
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueTable.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueTableConversion.java
similarity index 98%
rename from 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueTable.java
rename to 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueTableConversion.java
index 303d96786c..234f678fd3 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueTable.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestAwsGlueTableConversion.java
@@ -38,7 +38,7 @@ import software.amazon.awssdk.services.glue.model.Table;
 import software.amazon.awssdk.services.glue.model.TableInput;
 
 /**
- * Runs {@link AbstractGlueTableTest} scenarios against a real AWS Glue 
endpoint.
+ * Runs {@link GlueTableTestBase} scenarios against a real AWS Glue endpoint.
  *
  * <p>This test is <b>skipped by default</b> and only runs when {@code 
AWS_ACCESS_KEY_ID} is set. To
  * run it, set the following environment variables:
@@ -55,7 +55,7 @@ import software.amazon.awssdk.services.glue.model.TableInput;
  * deleted in {@link #cleanup} regardless of test outcome.
  */
 @EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = ".+")
-class TestAwsGlueTable extends AbstractGlueTableTest {
+class TestAwsGlueTableConversion extends GlueTableTestBase {
 
   private static GlueClient glueClient;
   private static String catalogId;
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
new file mode 100644
index 0000000000..89e0163e55
--- /dev/null
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchemaOperations.java
@@ -0,0 +1,314 @@
+/*
+ * 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.catalog.glue;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.SchemaChange;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.exceptions.NonEmptySchemaException;
+import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import software.amazon.awssdk.services.glue.GlueClient;
+import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
+import software.amazon.awssdk.services.glue.model.CreateDatabaseRequest;
+import software.amazon.awssdk.services.glue.model.Database;
+import software.amazon.awssdk.services.glue.model.DeleteDatabaseRequest;
+import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
+import software.amazon.awssdk.services.glue.model.GetDatabaseRequest;
+import software.amazon.awssdk.services.glue.model.GetDatabaseResponse;
+import software.amazon.awssdk.services.glue.model.GetDatabasesRequest;
+import software.amazon.awssdk.services.glue.model.GetDatabasesResponse;
+import software.amazon.awssdk.services.glue.model.GetTablesRequest;
+import software.amazon.awssdk.services.glue.model.GetTablesResponse;
+import software.amazon.awssdk.services.glue.model.Table;
+import software.amazon.awssdk.services.glue.model.UpdateDatabaseRequest;
+import software.amazon.awssdk.services.glue.model.UpdateDatabaseResponse;
+
+class TestGlueCatalogSchemaOperations {
+
+  private GlueCatalogOperations ops;
+  private GlueClient mockClient;
+
+  @BeforeEach
+  void setup() {
+    mockClient = mock(GlueClient.class);
+    ops = new GlueCatalogOperations();
+    ops.glueClient = mockClient;
+    // catalogId is null by default (caller's AWS account)
+  }
+
+  // -------------------------------------------------------------------------
+  // listSchemas
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testListSchemasPaginated() {
+    Namespace ns = Namespace.of("metalake", "catalog");
+    Database db1 = Database.builder().name("db1").build();
+    Database db2 = Database.builder().name("db2").build();
+    Database db3 = Database.builder().name("db3").build();
+    Database db4 = Database.builder().name("db4").build();
+
+    when(mockClient.getDatabases(any(GetDatabasesRequest.class)))
+        .thenReturn(
+            GetDatabasesResponse.builder().databaseList(db1, 
db2).nextToken("token1").build())
+        .thenReturn(GetDatabasesResponse.builder().databaseList(db3, 
db4).nextToken(null).build());
+
+    NameIdentifier[] result = ops.listSchemas(ns);
+
+    assertEquals(4, result.length);
+    assertEquals("db1", result[0].name());
+    assertEquals("db4", result[3].name());
+  }
+
+  @Test
+  void testListSchemasEmpty() {
+    Namespace ns = Namespace.of("metalake", "catalog");
+    when(mockClient.getDatabases(any(GetDatabasesRequest.class)))
+        .thenReturn(
+            GetDatabasesResponse.builder()
+                .databaseList(Collections.emptyList())
+                .nextToken(null)
+                .build());
+
+    NameIdentifier[] result = ops.listSchemas(ns);
+
+    assertEquals(0, result.length);
+  }
+
+  // -------------------------------------------------------------------------
+  // createSchema
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testCreateSchemaSuccess() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    Map<String, String> props = Map.of("k", "v");
+
+    GlueSchema schema = ops.createSchema(ident, "my comment", props);
+
+    verify(mockClient).createDatabase(any(CreateDatabaseRequest.class));
+    assertEquals("mydb", schema.name());
+    assertEquals("my comment", schema.comment());
+    assertEquals(props, schema.properties());
+  }
+
+  @Test
+  void testCreateSchemaAlreadyExists() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    when(mockClient.createDatabase(any(CreateDatabaseRequest.class)))
+        .thenThrow(AlreadyExistsException.builder().message("exists").build());
+
+    assertThrows(
+        SchemaAlreadyExistsException.class,
+        () -> ops.createSchema(ident, null, Collections.emptyMap()));
+  }
+
+  // -------------------------------------------------------------------------
+  // loadSchema
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testLoadSchemaSuccess() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    Database db =
+        Database.builder()
+            .name("mydb")
+            .description("desc")
+            .parameters(Map.of("k", "v"))
+            .createTime(Instant.now())
+            .build();
+    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
+        .thenReturn(GetDatabaseResponse.builder().database(db).build());
+
+    GlueSchema schema = ops.loadSchema(ident);
+
+    assertEquals("mydb", schema.name());
+    assertEquals("desc", schema.comment());
+    assertEquals(Map.of("k", "v"), schema.properties());
+  }
+
+  @Test
+  void testLoadSchemaNotFound() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "missing");
+    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
+        .thenThrow(EntityNotFoundException.builder().message("not 
found").build());
+
+    assertThrows(NoSuchSchemaException.class, () -> ops.loadSchema(ident));
+  }
+
+  // -------------------------------------------------------------------------
+  // alterSchema
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testAlterSchemaSetProperty() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    Database db =
+        Database.builder()
+            .name("mydb")
+            .parameters(Map.of("existing", "val"))
+            .createTime(Instant.now())
+            .build();
+    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
+        .thenReturn(GetDatabaseResponse.builder().database(db).build());
+    when(mockClient.updateDatabase(any(UpdateDatabaseRequest.class)))
+        .thenReturn(UpdateDatabaseResponse.builder().build());
+
+    ArgumentCaptor<UpdateDatabaseRequest> captor =
+        ArgumentCaptor.forClass(UpdateDatabaseRequest.class);
+
+    GlueSchema result = ops.alterSchema(ident, 
SchemaChange.setProperty("newKey", "newVal"));
+
+    verify(mockClient).updateDatabase(captor.capture());
+    Map<String, String> sentParams = 
captor.getValue().databaseInput().parameters();
+    assertEquals("val", sentParams.get("existing"));
+    assertEquals("newVal", sentParams.get("newKey"));
+    assertEquals("newVal", result.properties().get("newKey"));
+  }
+
+  @Test
+  void testAlterSchemaRemoveProperty() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    Database db =
+        Database.builder()
+            .name("mydb")
+            .parameters(Map.of("toRemove", "v"))
+            .createTime(Instant.now())
+            .build();
+    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
+        .thenReturn(GetDatabaseResponse.builder().database(db).build());
+    when(mockClient.updateDatabase(any(UpdateDatabaseRequest.class)))
+        .thenReturn(UpdateDatabaseResponse.builder().build());
+
+    ArgumentCaptor<UpdateDatabaseRequest> captor =
+        ArgumentCaptor.forClass(UpdateDatabaseRequest.class);
+
+    ops.alterSchema(ident, SchemaChange.removeProperty("toRemove"));
+
+    verify(mockClient).updateDatabase(captor.capture());
+    
assertFalse(captor.getValue().databaseInput().parameters().containsKey("toRemove"));
+  }
+
+  @Test
+  void testAlterSchemaUnsupportedChange() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    Database db =
+        
Database.builder().name("mydb").parameters(Map.of()).createTime(Instant.now()).build();
+    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
+        .thenReturn(GetDatabaseResponse.builder().database(db).build());
+
+    SchemaChange unsupported = mock(SchemaChange.class);
+
+    assertThrows(IllegalArgumentException.class, () -> ops.alterSchema(ident, 
unsupported));
+  }
+
+  @Test
+  void testAlterSchemaNotFound() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "missing");
+    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
+        .thenThrow(EntityNotFoundException.builder().message("not 
found").build());
+
+    assertThrows(
+        NoSuchSchemaException.class,
+        () -> ops.alterSchema(ident, SchemaChange.setProperty("k", "v")));
+  }
+
+  // -------------------------------------------------------------------------
+  // dropSchema
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testDropSchemaCascadeFalseEmpty() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    when(mockClient.getTables(any(GetTablesRequest.class)))
+        .thenReturn(GetTablesResponse.builder().tableList(List.of()).build());
+
+    boolean dropped = ops.dropSchema(ident, false);
+
+    verify(mockClient).deleteDatabase(any(DeleteDatabaseRequest.class));
+    assertTrue(dropped);
+  }
+
+  @Test
+  void testDropSchemaCascadeFalseNonEmpty() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    Table t = Table.builder().name("t1").build();
+    when(mockClient.getTables(any(GetTablesRequest.class)))
+        .thenReturn(GetTablesResponse.builder().tableList(t).build());
+
+    assertThrows(NonEmptySchemaException.class, () -> ops.dropSchema(ident, 
false));
+    verify(mockClient, 
never()).deleteDatabase(any(DeleteDatabaseRequest.class));
+  }
+
+  @Test
+  void testDropSchemaCascadeTrue() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+
+    boolean dropped = ops.dropSchema(ident, true);
+
+    verify(mockClient, never()).getTables(any(GetTablesRequest.class));
+    verify(mockClient).deleteDatabase(any(DeleteDatabaseRequest.class));
+    assertTrue(dropped);
+  }
+
+  @Test
+  void testDropSchemaNotFound() {
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "missing");
+    when(mockClient.deleteDatabase(any(DeleteDatabaseRequest.class)))
+        .thenThrow(EntityNotFoundException.builder().message("not 
found").build());
+
+    boolean dropped = ops.dropSchema(ident, true);
+
+    assertFalse(dropped);
+  }
+
+  @Test
+  void testDropSchemaWithCatalogId() {
+    ops.catalogId = "123456789012";
+    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
+    when(mockClient.getTables(any(GetTablesRequest.class)))
+        .thenReturn(GetTablesResponse.builder().tableList(List.of()).build());
+
+    ArgumentCaptor<DeleteDatabaseRequest> captor =
+        ArgumentCaptor.forClass(DeleteDatabaseRequest.class);
+
+    ops.dropSchema(ident, false);
+
+    verify(mockClient).deleteDatabase(captor.capture());
+    assertEquals("123456789012", captor.getValue().catalogId());
+  }
+}
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchema.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogTableOperations.java
similarity index 58%
rename from 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchema.java
rename to 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogTableOperations.java
index 487d120684..4e390f1144 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogSchema.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueCatalogTableOperations.java
@@ -24,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -32,19 +31,18 @@ import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
-import org.apache.gravitino.SchemaChange;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.exceptions.NoSuchTableException;
-import org.apache.gravitino.exceptions.NonEmptySchemaException;
-import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
 import org.apache.gravitino.exceptions.TableAlreadyExistsException;
 import org.apache.gravitino.rel.Column;
 import org.apache.gravitino.rel.TableChange;
 import org.apache.gravitino.rel.expressions.distributions.Distributions;
 import org.apache.gravitino.rel.expressions.sorts.SortOrders;
 import org.apache.gravitino.rel.expressions.transforms.Transforms;
+import org.apache.gravitino.rel.indexes.Index;
 import org.apache.gravitino.rel.indexes.Indexes;
 import org.apache.gravitino.rel.types.Types;
 import org.junit.jupiter.api.BeforeEach;
@@ -52,28 +50,19 @@ import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 import software.amazon.awssdk.services.glue.GlueClient;
 import software.amazon.awssdk.services.glue.model.AlreadyExistsException;
-import software.amazon.awssdk.services.glue.model.CreateDatabaseRequest;
 import software.amazon.awssdk.services.glue.model.CreateTableRequest;
-import software.amazon.awssdk.services.glue.model.Database;
-import software.amazon.awssdk.services.glue.model.DeleteDatabaseRequest;
 import software.amazon.awssdk.services.glue.model.DeleteTableRequest;
 import software.amazon.awssdk.services.glue.model.EntityNotFoundException;
-import software.amazon.awssdk.services.glue.model.GetDatabaseRequest;
-import software.amazon.awssdk.services.glue.model.GetDatabaseResponse;
-import software.amazon.awssdk.services.glue.model.GetDatabasesRequest;
-import software.amazon.awssdk.services.glue.model.GetDatabasesResponse;
 import software.amazon.awssdk.services.glue.model.GetTableRequest;
 import software.amazon.awssdk.services.glue.model.GetTableResponse;
 import software.amazon.awssdk.services.glue.model.GetTablesRequest;
 import software.amazon.awssdk.services.glue.model.GetTablesResponse;
 import software.amazon.awssdk.services.glue.model.StorageDescriptor;
 import software.amazon.awssdk.services.glue.model.Table;
-import software.amazon.awssdk.services.glue.model.UpdateDatabaseRequest;
-import software.amazon.awssdk.services.glue.model.UpdateDatabaseResponse;
 import software.amazon.awssdk.services.glue.model.UpdateTableRequest;
 import software.amazon.awssdk.services.glue.model.UpdateTableResponse;
 
-class TestGlueCatalogSchema {
+class TestGlueCatalogTableOperations {
 
   private GlueCatalogOperations ops;
   private GlueClient mockClient;
@@ -85,250 +74,12 @@ class TestGlueCatalogSchema {
     ops.glueClient = mockClient;
   }
 
-  // -------------------------------------------------------------------------
-  // listSchemas
-  // -------------------------------------------------------------------------
-
-  @Test
-  void testListSchemas() {
-    Namespace ns = Namespace.of("metalake", "catalog");
-    Database db1 = Database.builder().name("db1").build();
-    Database db2 = Database.builder().name("db2").build();
-    Database db3 = Database.builder().name("db3").build();
-    Database db4 = Database.builder().name("db4").build();
-
-    when(mockClient.getDatabases(any(GetDatabasesRequest.class)))
-        .thenReturn(
-            GetDatabasesResponse.builder().databaseList(db1, 
db2).nextToken("token1").build())
-        .thenReturn(GetDatabasesResponse.builder().databaseList(db3, 
db4).nextToken(null).build());
-
-    NameIdentifier[] result = ops.listSchemas(ns);
-
-    assertEquals(4, result.length);
-    assertEquals("db1", result[0].name());
-    assertEquals("db4", result[3].name());
-  }
-
-  @Test
-  void testListSchemasReturnsEmptyArray() {
-    Namespace ns = Namespace.of("metalake", "catalog");
-    when(mockClient.getDatabases(any(GetDatabasesRequest.class)))
-        .thenReturn(
-            GetDatabasesResponse.builder()
-                .databaseList(Collections.emptyList())
-                .nextToken(null)
-                .build());
-
-    assertEquals(0, ops.listSchemas(ns).length);
-  }
-
-  // -------------------------------------------------------------------------
-  // createSchema
-  // -------------------------------------------------------------------------
-
-  @Test
-  void testCreateSchema() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    Map<String, String> props = Map.of("k", "v");
-
-    GlueSchema schema = ops.createSchema(ident, "my comment", props);
-
-    verify(mockClient).createDatabase(any(CreateDatabaseRequest.class));
-    assertEquals("mydb", schema.name());
-    assertEquals("my comment", schema.comment());
-    assertEquals(props, schema.properties());
-  }
-
-  @Test
-  void testCreateSchemaAlreadyExists() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    when(mockClient.createDatabase(any(CreateDatabaseRequest.class)))
-        .thenThrow(AlreadyExistsException.builder().message("exists").build());
-
-    assertThrows(
-        SchemaAlreadyExistsException.class,
-        () -> ops.createSchema(ident, null, Collections.emptyMap()));
-  }
-
-  // -------------------------------------------------------------------------
-  // loadSchema
-  // -------------------------------------------------------------------------
-
-  @Test
-  void testLoadSchema() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    Database db =
-        Database.builder()
-            .name("mydb")
-            .description("desc")
-            .parameters(Map.of("k", "v"))
-            .createTime(Instant.now())
-            .build();
-    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
-        .thenReturn(GetDatabaseResponse.builder().database(db).build());
-
-    GlueSchema schema = ops.loadSchema(ident);
-
-    assertEquals("mydb", schema.name());
-    assertEquals("desc", schema.comment());
-    assertEquals(Map.of("k", "v"), schema.properties());
-  }
-
-  @Test
-  void testLoadSchemaNotFound() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "missing");
-    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
-        .thenThrow(EntityNotFoundException.builder().message("not 
found").build());
-
-    assertThrows(NoSuchSchemaException.class, () -> ops.loadSchema(ident));
-  }
-
-  // -------------------------------------------------------------------------
-  // alterSchema
-  // -------------------------------------------------------------------------
-
-  @Test
-  void testAlterSchemaSetProperty() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    Database db =
-        Database.builder()
-            .name("mydb")
-            .parameters(Map.of("existing", "val"))
-            .createTime(Instant.now())
-            .build();
-    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
-        .thenReturn(GetDatabaseResponse.builder().database(db).build());
-    when(mockClient.updateDatabase(any(UpdateDatabaseRequest.class)))
-        .thenReturn(UpdateDatabaseResponse.builder().build());
-
-    ArgumentCaptor<UpdateDatabaseRequest> captor =
-        ArgumentCaptor.forClass(UpdateDatabaseRequest.class);
-
-    GlueSchema result = ops.alterSchema(ident, 
SchemaChange.setProperty("newKey", "newVal"));
-
-    verify(mockClient).updateDatabase(captor.capture());
-    Map<String, String> sentParams = 
captor.getValue().databaseInput().parameters();
-    assertEquals("val", sentParams.get("existing"));
-    assertEquals("newVal", sentParams.get("newKey"));
-    assertEquals("newVal", result.properties().get("newKey"));
-  }
-
-  @Test
-  void testAlterSchemaRemoveProperty() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    Database db =
-        Database.builder()
-            .name("mydb")
-            .parameters(Map.of("toRemove", "v"))
-            .createTime(Instant.now())
-            .build();
-    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
-        .thenReturn(GetDatabaseResponse.builder().database(db).build());
-    when(mockClient.updateDatabase(any(UpdateDatabaseRequest.class)))
-        .thenReturn(UpdateDatabaseResponse.builder().build());
-
-    ArgumentCaptor<UpdateDatabaseRequest> captor =
-        ArgumentCaptor.forClass(UpdateDatabaseRequest.class);
-
-    ops.alterSchema(ident, SchemaChange.removeProperty("toRemove"));
-
-    verify(mockClient).updateDatabase(captor.capture());
-    
assertFalse(captor.getValue().databaseInput().parameters().containsKey("toRemove"));
-  }
-
-  @Test
-  void testAlterSchemaUnsupportedChange() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    Database db =
-        
Database.builder().name("mydb").parameters(Map.of()).createTime(Instant.now()).build();
-    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
-        .thenReturn(GetDatabaseResponse.builder().database(db).build());
-
-    SchemaChange unsupported = mock(SchemaChange.class);
-
-    assertThrows(IllegalArgumentException.class, () -> ops.alterSchema(ident, 
unsupported));
-  }
-
-  @Test
-  void testAlterSchemaNotFound() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "missing");
-    when(mockClient.getDatabase(any(GetDatabaseRequest.class)))
-        .thenThrow(EntityNotFoundException.builder().message("not 
found").build());
-
-    assertThrows(
-        NoSuchSchemaException.class,
-        () -> ops.alterSchema(ident, SchemaChange.setProperty("k", "v")));
-  }
-
-  // -------------------------------------------------------------------------
-  // dropSchema
-  // -------------------------------------------------------------------------
-
-  @Test
-  void testDropSchema() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    when(mockClient.getTables(any(GetTablesRequest.class)))
-        .thenReturn(GetTablesResponse.builder().tableList(List.of()).build());
-
-    boolean dropped = ops.dropSchema(ident, false);
-
-    verify(mockClient).deleteDatabase(any(DeleteDatabaseRequest.class));
-    assertTrue(dropped);
-  }
-
-  @Test
-  void testDropSchemaNonEmpty() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    Table t = Table.builder().name("t1").build();
-    when(mockClient.getTables(any(GetTablesRequest.class)))
-        .thenReturn(GetTablesResponse.builder().tableList(t).build());
-
-    assertThrows(NonEmptySchemaException.class, () -> ops.dropSchema(ident, 
false));
-    verify(mockClient, 
never()).deleteDatabase(any(DeleteDatabaseRequest.class));
-  }
-
-  @Test
-  void testDropSchemaCascadeTrue() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-
-    boolean dropped = ops.dropSchema(ident, true);
-
-    verify(mockClient, never()).getTables(any(GetTablesRequest.class));
-    verify(mockClient).deleteDatabase(any(DeleteDatabaseRequest.class));
-    assertTrue(dropped);
-  }
-
-  @Test
-  void testDropSchemaNotFound() {
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "missing");
-    when(mockClient.deleteDatabase(any(DeleteDatabaseRequest.class)))
-        .thenThrow(EntityNotFoundException.builder().message("not 
found").build());
-
-    assertFalse(ops.dropSchema(ident, true));
-  }
-
-  @Test
-  void testDropSchemaWithCatalogId() {
-    ops.catalogId = "123456789012";
-    NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb");
-    when(mockClient.getTables(any(GetTablesRequest.class)))
-        .thenReturn(GetTablesResponse.builder().tableList(List.of()).build());
-
-    ArgumentCaptor<DeleteDatabaseRequest> captor =
-        ArgumentCaptor.forClass(DeleteDatabaseRequest.class);
-
-    ops.dropSchema(ident, false);
-
-    verify(mockClient).deleteDatabase(captor.capture());
-    assertEquals("123456789012", captor.getValue().catalogId());
-  }
-
   // -------------------------------------------------------------------------
   // listTables
   // -------------------------------------------------------------------------
 
   @Test
-  void testListTables() {
+  void testListTablesPaginated() {
     Namespace ns = Namespace.of("metalake", "catalog", "mydb");
     Table t1 = Table.builder().name("t1").build();
     Table t2 = Table.builder().name("t2").build();
@@ -355,8 +106,8 @@ class TestGlueCatalogSchema {
   }
 
   @Test
-  void testListTablesWithFormatFilter() {
-    ops.tableFormatFilter = java.util.Set.of("iceberg");
+  void testListTablesFormatFilter() {
+    ops.tableFormatFilter = Set.of("iceberg");
     Namespace ns = Namespace.of("metalake", "catalog", "mydb");
 
     Table icebergTable =
@@ -381,7 +132,7 @@ class TestGlueCatalogSchema {
   // -------------------------------------------------------------------------
 
   @Test
-  void testLoadTable() {
+  void testLoadTableSuccess() {
     NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb", 
"mytable");
     Table glueTable =
         Table.builder()
@@ -422,7 +173,7 @@ class TestGlueCatalogSchema {
   // -------------------------------------------------------------------------
 
   @Test
-  void testCreateTable() {
+  void testCreateTableSuccess() {
     NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb", 
"mytable");
     Column[] columns = {
       
GlueColumn.builder().withName("id").withType(Types.LongType.get()).withNullable(true).build(),
@@ -471,7 +222,7 @@ class TestGlueCatalogSchema {
   }
 
   @Test
-  void testCreateTableRejectsIndexes() {
+  void testCreateTableIndexesRejected() {
     NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb", 
"mytable");
 
     assertThrows(
@@ -485,9 +236,7 @@ class TestGlueCatalogSchema {
                 Transforms.EMPTY_TRANSFORM,
                 Distributions.NONE,
                 SortOrders.NONE,
-                new org.apache.gravitino.rel.indexes.Index[] {
-                  mock(org.apache.gravitino.rel.indexes.Index.class)
-                }));
+                new Index[] {mock(Index.class)}));
   }
 
   @Test
@@ -526,7 +275,7 @@ class TestGlueCatalogSchema {
   // -------------------------------------------------------------------------
 
   @Test
-  void testAlterTable() {
+  void testAlterTableRenameAndComment() {
     NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb", 
"old");
     Table glueTable =
         Table.builder()
@@ -616,11 +365,13 @@ class TestGlueCatalogSchema {
   // -------------------------------------------------------------------------
 
   @Test
-  void testDropTable() {
+  void testDropTableSuccess() {
     NameIdentifier ident = NameIdentifier.of("metalake", "catalog", "mydb", 
"t");
 
-    assertTrue(ops.dropTable(ident));
+    boolean result = ops.dropTable(ident);
+
     verify(mockClient).deleteTable(any(DeleteTableRequest.class));
+    assertTrue(result);
   }
 
   @Test
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTableOperations.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGluePartitionOperations.java
similarity index 99%
rename from 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTableOperations.java
rename to 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGluePartitionOperations.java
index 14e294eef5..0099b39512 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTableOperations.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGluePartitionOperations.java
@@ -47,7 +47,7 @@ import 
software.amazon.awssdk.services.glue.model.GetPartitionResponse;
 import software.amazon.awssdk.services.glue.model.GetPartitionsRequest;
 import software.amazon.awssdk.services.glue.model.GetPartitionsResponse;
 
-class TestGlueTableOperations {
+class TestGluePartitionOperations {
 
   private GlueClient mockClient;
   private GlueTableOperations ops;
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestSyntheticGlueSchema.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueSchemaConversion.java
similarity index 90%
rename from 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestSyntheticGlueSchema.java
rename to 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueSchemaConversion.java
index a1c0643d8c..be790425bc 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestSyntheticGlueSchema.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueSchemaConversion.java
@@ -23,13 +23,13 @@ import java.util.Map;
 import software.amazon.awssdk.services.glue.model.Database;
 
 /**
- * Runs {@link AbstractGlueSchemaTest} scenarios using AWS SDK builders to 
create {@link Database}
+ * Runs {@link GlueSchemaTestBase} scenarios using AWS SDK builders to create 
{@link Database}
  * objects directly — no network or AWS credentials required.
  *
  * <p>This verifies that the {@link GlueSchema#fromGlueDatabase} conversion 
logic works correctly
  * for typical Glue API response shapes.
  */
-class TestSyntheticGlueSchema extends AbstractGlueSchemaTest {
+class TestGlueSchemaConversion extends GlueSchemaTestBase {
 
   @Override
   protected Database provideDatabase(String name, String description, 
Map<String, String> params) {
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestSyntheticGlueTable.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTableConversion.java
similarity index 95%
rename from 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestSyntheticGlueTable.java
rename to 
catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTableConversion.java
index d32eb80025..085b7f7d9a 100644
--- 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestSyntheticGlueTable.java
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/TestGlueTableConversion.java
@@ -27,10 +27,10 @@ import 
software.amazon.awssdk.services.glue.model.StorageDescriptor;
 import software.amazon.awssdk.services.glue.model.Table;
 
 /**
- * Runs {@link AbstractGlueTableTest} scenarios using AWS SDK builders — no 
network or credentials
+ * Runs {@link GlueTableTestBase} scenarios using AWS SDK builders — no 
network or credentials
  * required.
  */
-class TestSyntheticGlueTable extends AbstractGlueTableTest {
+class TestGlueTableConversion extends GlueTableTestBase {
 
   private static final String INPUT_FMT = 
"org.apache.hadoop.mapred.TextInputFormat";
   private static final String OUTPUT_FMT =
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AbstractGlueCatalogIT.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AbstractGlueCatalogIT.java
new file mode 100644
index 0000000000..93e25d9b0d
--- /dev/null
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AbstractGlueCatalogIT.java
@@ -0,0 +1,661 @@
+/*
+ * 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.catalog.glue.integration.test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.Schema;
+import org.apache.gravitino.SchemaChange;
+import org.apache.gravitino.catalog.glue.GlueCatalogOperations;
+import org.apache.gravitino.catalog.glue.GlueConstants;
+import org.apache.gravitino.exceptions.NonEmptySchemaException;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.SupportsPartitions;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.expressions.NamedReference;
+import org.apache.gravitino.rel.expressions.distributions.Distributions;
+import org.apache.gravitino.rel.expressions.literals.Literal;
+import org.apache.gravitino.rel.expressions.literals.Literals;
+import org.apache.gravitino.rel.expressions.sorts.SortDirection;
+import org.apache.gravitino.rel.expressions.sorts.SortOrder;
+import org.apache.gravitino.rel.expressions.sorts.SortOrders;
+import org.apache.gravitino.rel.expressions.transforms.Transform;
+import org.apache.gravitino.rel.expressions.transforms.Transforms;
+import org.apache.gravitino.rel.indexes.Index;
+import org.apache.gravitino.rel.partitions.Partition;
+import org.apache.gravitino.rel.partitions.Partitions;
+import org.apache.gravitino.rel.types.Types;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * Abstract base class for Glue catalog integration tests.
+ *
+ * <p>Subclasses provide backend-specific configuration via {@link 
#catalogConfig()}. All test
+ * scenarios are defined here and shared between backend implementations.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+abstract class AbstractGlueCatalogIT {
+
+  protected GlueCatalogOperations ops;
+  private String currentSchema;
+
+  private static final Namespace SCHEMA_NS = Namespace.of("ml", "cat");
+  private static final String INPUT_FMT = 
"org.apache.hadoop.mapred.TextInputFormat";
+  private static final String OUTPUT_FMT =
+      "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat";
+  private static final String SERDE = 
"org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe";
+
+  protected abstract Map<String, String> catalogConfig();
+
+  @BeforeAll
+  void initOps() {
+    ops = new GlueCatalogOperations();
+    ops.initialize(catalogConfig(), null, null);
+  }
+
+  @AfterAll
+  void closeOps() throws Exception {
+    if (ops != null) {
+      ops.close();
+    }
+  }
+
+  @AfterEach
+  void cleanupSchema() {
+    if (currentSchema != null) {
+      try {
+        ops.dropSchema(schemaIdent(currentSchema), true);
+      } catch (Exception ignored) {
+      }
+      currentSchema = null;
+    }
+  }
+
+  // -------------------------------------------------------------------------
+  // Helpers
+  // -------------------------------------------------------------------------
+
+  private String newSchema() {
+    String name = "glue_it_" + System.nanoTime();
+    currentSchema = name;
+    return name;
+  }
+
+  private NameIdentifier schemaIdent(String name) {
+    return NameIdentifier.of("ml", "cat", name);
+  }
+
+  private Namespace tableNs(String schema) {
+    return Namespace.of("ml", "cat", schema);
+  }
+
+  private NameIdentifier tableIdent(String schema, String table) {
+    return NameIdentifier.of("ml", "cat", schema, table);
+  }
+
+  private Column[] hiveColumns() {
+    return new Column[] {
+      Column.of("id", Types.LongType.get(), "primary key", false, false, null),
+      Column.of("name", Types.StringType.get(), null),
+    };
+  }
+
+  private Map<String, String> hiveTableProps() {
+    Map<String, String> props = new HashMap<>();
+    props.put(GlueConstants.TABLE_TYPE, "EXTERNAL_TABLE");
+    props.put(GlueConstants.INPUT_FORMAT, INPUT_FMT);
+    props.put(GlueConstants.OUTPUT_FORMAT, OUTPUT_FMT);
+    props.put(GlueConstants.SERDE_LIB, SERDE);
+    return props;
+  }
+
+  private Table createHiveTable(String schema, String table) {
+    return ops.createTable(
+        tableIdent(schema, table),
+        hiveColumns(),
+        null,
+        hiveTableProps(),
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+  }
+
+  private Column[] partitionedColumns() {
+    return new Column[] {
+      Column.of("id", Types.LongType.get(), null, false, false, null),
+      Column.of("dt", Types.DateType.get(), null),
+    };
+  }
+
+  private SupportsPartitions createPartitionedTable(String schema, String 
table) {
+    Map<String, String> props = hiveTableProps();
+    props.put(GlueConstants.LOCATION, "s3://gravitino-test-bucket/" + schema + 
"/" + table);
+    ops.createTable(
+        tableIdent(schema, table),
+        partitionedColumns(),
+        null,
+        props,
+        new Transform[] {Transforms.identity("dt")},
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+    return ops.loadTable(tableIdent(schema, table)).supportPartitions();
+  }
+
+  private Partition identityPartition(String dateValue) {
+    return Partitions.identity(
+        "dt=" + dateValue,
+        new String[][] {{"dt"}},
+        new Literal[] {Literals.stringLiteral(dateValue)},
+        Collections.emptyMap());
+  }
+
+  // -------------------------------------------------------------------------
+  // Schema tests
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testSchemaProperties() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), "test comment", Map.of("k1", "v1"));
+
+    Schema loaded = ops.loadSchema(schemaIdent(schema));
+    assertEquals(schema, loaded.name());
+    assertEquals("test comment", loaded.comment());
+    assertTrue(loaded.properties().containsKey("k1"));
+    assertEquals("v1", loaded.properties().get("k1"));
+  }
+
+  @Test
+  void testListSchemas() {
+    String schema1 = newSchema();
+    String schema2 = "glue_it_" + System.nanoTime() + "_b";
+    ops.createSchema(schemaIdent(schema1), null, Collections.emptyMap());
+    ops.createSchema(schemaIdent(schema2), null, Collections.emptyMap());
+    try {
+      List<String> names =
+          Arrays.stream(ops.listSchemas(SCHEMA_NS))
+              .map(NameIdentifier::name)
+              .collect(Collectors.toList());
+      assertTrue(names.contains(schema1));
+      assertTrue(names.contains(schema2));
+    } finally {
+      try {
+        ops.dropSchema(schemaIdent(schema2), false);
+      } catch (Exception ignored) {
+      }
+    }
+  }
+
+  @Test
+  void testAlterSchema() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    ops.alterSchema(schemaIdent(schema), SchemaChange.setProperty("newKey", 
"newVal"));
+    Schema loaded = ops.loadSchema(schemaIdent(schema));
+    assertEquals("newVal", loaded.properties().get("newKey"));
+
+    ops.alterSchema(schemaIdent(schema), 
SchemaChange.removeProperty("newKey"));
+    Schema reloaded = ops.loadSchema(schemaIdent(schema));
+    assertFalse(reloaded.properties().containsKey("newKey"));
+  }
+
+  @Test
+  void testDropSchemaEmpty() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    boolean dropped = ops.dropSchema(schemaIdent(schema), false);
+    assertTrue(dropped);
+    currentSchema = null;
+  }
+
+  @Test
+  void testDropSchemaNonEmpty() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    createHiveTable(schema, "tbl");
+
+    try {
+      assertThrows(NonEmptySchemaException.class, () -> 
ops.dropSchema(schemaIdent(schema), false));
+
+      assertTrue(ops.dropTable(tableIdent(schema, "tbl")));
+      assertTrue(ops.dropSchema(schemaIdent(schema), false));
+      currentSchema = null;
+    } finally {
+      if (currentSchema != null) {
+        try {
+          ops.dropTable(tableIdent(schema, "tbl"));
+        } catch (Exception ignored) {
+        }
+        try {
+          ops.dropSchema(schemaIdent(schema), false);
+        } catch (Exception ignored) {
+        }
+        currentSchema = null;
+      }
+    }
+  }
+
+  // -------------------------------------------------------------------------
+  // Hive table tests
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testCreateHiveTable() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    createHiveTable(schema, "hive_tbl");
+
+    Table loaded = ops.loadTable(tableIdent(schema, "hive_tbl"));
+    assertEquals("hive_tbl", loaded.name());
+    assertEquals(2, loaded.columns().length);
+    assertEquals("id", loaded.columns()[0].name());
+    assertEquals(Types.LongType.get(), loaded.columns()[0].dataType());
+    assertEquals("EXTERNAL_TABLE", 
loaded.properties().get(GlueConstants.TABLE_TYPE));
+    assertNotNull(loaded.properties().get(GlueConstants.INPUT_FORMAT));
+    assertNotNull(loaded.properties().get(GlueConstants.OUTPUT_FORMAT));
+    assertNotNull(loaded.properties().get(GlueConstants.SERDE_LIB));
+  }
+
+  @Test
+  void testListTables() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    createHiveTable(schema, "tbl1");
+    createHiveTable(schema, "tbl2");
+
+    List<String> names =
+        Arrays.stream(ops.listTables(tableNs(schema)))
+            .map(NameIdentifier::name)
+            .collect(Collectors.toList());
+    assertTrue(names.contains("tbl1"));
+    assertTrue(names.contains("tbl2"));
+  }
+
+  @Test
+  void testAlterTable() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    createHiveTable(schema, "altertbl");
+
+    ops.alterTable(
+        tableIdent(schema, "altertbl"),
+        TableChange.addColumn(new String[] {"email"}, Types.StringType.get()));
+    Table altered = ops.loadTable(tableIdent(schema, "altertbl"));
+    assertEquals(3, altered.columns().length);
+    assertEquals("email", altered.columns()[2].name());
+  }
+
+  @Test
+  void testDropTable() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    createHiveTable(schema, "droptbl");
+
+    assertTrue(ops.dropTable(tableIdent(schema, "droptbl")));
+    assertFalse(ops.dropTable(tableIdent(schema, "droptbl")));
+  }
+
+  // -------------------------------------------------------------------------
+  // Iceberg table tests
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testCreateIcebergTable() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    Map<String, String> props = new HashMap<>();
+    props.put(GlueConstants.TABLE_FORMAT, "ICEBERG");
+    props.put(GlueConstants.METADATA_LOCATION, 
"s3://bucket/path/metadata/v1.metadata.json");
+    ops.createTable(
+        tableIdent(schema, "iceberg_tbl"),
+        new Column[0],
+        null,
+        props,
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    Table loaded = ops.loadTable(tableIdent(schema, "iceberg_tbl"));
+    assertEquals("ICEBERG", 
loaded.properties().get(GlueConstants.TABLE_FORMAT));
+    assertEquals(
+        "s3://bucket/path/metadata/v1.metadata.json",
+        loaded.properties().get(GlueConstants.METADATA_LOCATION));
+    assertEquals(0, loaded.columns().length);
+  }
+
+  // -------------------------------------------------------------------------
+  // Partition tests
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testAddPartition() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    SupportsPartitions sp = createPartitionedTable(schema, "ptbl");
+
+    sp.addPartition(identityPartition("2024-01-01"));
+
+    List<String> names = Arrays.asList(sp.listPartitionNames());
+    assertTrue(names.contains("dt=2024-01-01"));
+  }
+
+  @Test
+  void testListPartitions() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    SupportsPartitions sp = createPartitionedTable(schema, "ptbl");
+
+    sp.addPartition(identityPartition("2024-02-01"));
+
+    Partition[] parts = sp.listPartitions();
+    assertEquals(1, parts.length);
+    assertEquals("dt=2024-02-01", parts[0].name());
+  }
+
+  @Test
+  void testGetPartition() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    SupportsPartitions sp = createPartitionedTable(schema, "ptbl");
+
+    sp.addPartition(identityPartition("2024-03-01"));
+
+    Partition p = sp.getPartition("dt=2024-03-01");
+    assertNotNull(p);
+    assertEquals("dt=2024-03-01", p.name());
+  }
+
+  @Test
+  void testDropPartition() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+    SupportsPartitions sp = createPartitionedTable(schema, "ptbl");
+
+    sp.addPartition(identityPartition("2024-04-01"));
+
+    assertTrue(sp.dropPartition("dt=2024-04-01"));
+    assertFalse(sp.dropPartition("dt=2024-04-01"));
+  }
+
+  // -------------------------------------------------------------------------
+  // Hive table feature tests
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testHashDistribution() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    ops.createTable(
+        tableIdent(schema, "bucketed"),
+        hiveColumns(),
+        null,
+        hiveTableProps(),
+        new Transform[0],
+        Distributions.hash(4, NamedReference.field("id")),
+        new SortOrder[0],
+        new Index[0]);
+
+    Table loaded = ops.loadTable(tableIdent(schema, "bucketed"));
+    assertEquals(4, loaded.distribution().number());
+    assertEquals(1, loaded.distribution().expressions().length);
+    assertEquals(NamedReference.field("id"), 
loaded.distribution().expressions()[0]);
+  }
+
+  @Test
+  void testSortOrders() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    ops.createTable(
+        tableIdent(schema, "sorted"),
+        hiveColumns(),
+        null,
+        hiveTableProps(),
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[] {
+          SortOrders.ascending(NamedReference.field("id")),
+          SortOrders.descending(NamedReference.field("name"))
+        },
+        new Index[0]);
+
+    Table loaded = ops.loadTable(tableIdent(schema, "sorted"));
+    assertEquals(2, loaded.sortOrder().length);
+    assertEquals(SortDirection.ASCENDING, loaded.sortOrder()[0].direction());
+    assertEquals(NamedReference.field("id"), 
loaded.sortOrder()[0].expression());
+    assertEquals(SortDirection.DESCENDING, loaded.sortOrder()[1].direction());
+    assertEquals(NamedReference.field("name"), 
loaded.sortOrder()[1].expression());
+  }
+
+  @Test
+  void testTableComment() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    ops.createTable(
+        tableIdent(schema, "commented"),
+        hiveColumns(),
+        "my table comment",
+        hiveTableProps(),
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    Table loaded = ops.loadTable(tableIdent(schema, "commented"));
+    assertEquals("my table comment", loaded.comment());
+  }
+
+  @Test
+  void testUnsupportedTransformRejected() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            ops.createTable(
+                tableIdent(schema, "bad"),
+                hiveColumns(),
+                null,
+                hiveTableProps(),
+                new Transform[] {Transforms.year("dt")},
+                Distributions.NONE,
+                new SortOrder[0],
+                new Index[0]));
+  }
+
+  @Test
+  void testDecimalAndVarcharColumns() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    Column[] cols =
+        new Column[] {
+          Column.of("price", Types.DecimalType.of(10, 2), null),
+          Column.of("label", Types.VarCharType.of(255), null),
+        };
+    ops.createTable(
+        tableIdent(schema, "typed"),
+        cols,
+        null,
+        hiveTableProps(),
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    Table loaded = ops.loadTable(tableIdent(schema, "typed"));
+    assertEquals(Types.DecimalType.of(10, 2), loaded.columns()[0].dataType());
+    assertEquals(Types.VarCharType.of(255), loaded.columns()[1].dataType());
+  }
+
+  @Test
+  void testComplexColumnTypes() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    Column[] cols =
+        new Column[] {
+          Column.of("tags", Types.ExternalType.of("array<string>"), null),
+          Column.of("info", 
Types.ExternalType.of("struct<name:string,age:int>"), null),
+        };
+    ops.createTable(
+        tableIdent(schema, "complex"),
+        cols,
+        null,
+        hiveTableProps(),
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    Table loaded = ops.loadTable(tableIdent(schema, "complex"));
+    // GlueTypeConverter.toGravitino converts known complex types to native 
types.
+    assertEquals(Types.ListType.class, 
loaded.columns()[0].dataType().getClass());
+    assertEquals(Types.StructType.class, 
loaded.columns()[1].dataType().getClass());
+  }
+
+  @Test
+  void testSerDeParameters() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    Map<String, String> props = hiveTableProps();
+    props.put("serde.parameter.serialization.format", "1");
+    ops.createTable(
+        tableIdent(schema, "serde_tbl"),
+        hiveColumns(),
+        null,
+        props,
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    Table loaded = ops.loadTable(tableIdent(schema, "serde_tbl"));
+    assertEquals("1", 
loaded.properties().get("serde.parameter.serialization.format"));
+  }
+
+  // -------------------------------------------------------------------------
+  // Iceberg table tests (additional)
+  // -------------------------------------------------------------------------
+
+  @Test
+  void testDropIcebergTable() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    Map<String, String> props = new HashMap<>();
+    props.put(GlueConstants.TABLE_FORMAT, "ICEBERG");
+    props.put(GlueConstants.METADATA_LOCATION, 
"s3://bucket/path/metadata/v1.metadata.json");
+    ops.createTable(
+        tableIdent(schema, "iceberg_drop"),
+        new Column[0],
+        null,
+        props,
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    assertTrue(ops.dropTable(tableIdent(schema, "iceberg_drop")));
+    assertFalse(ops.dropTable(tableIdent(schema, "iceberg_drop")));
+  }
+
+  @Test
+  void testAlterIcebergMetadata() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    Map<String, String> props = new HashMap<>();
+    props.put(GlueConstants.TABLE_FORMAT, "ICEBERG");
+    props.put(GlueConstants.METADATA_LOCATION, 
"s3://bucket/path/metadata/v1.metadata.json");
+    ops.createTable(
+        tableIdent(schema, "iceberg_alter"),
+        new Column[0],
+        null,
+        props,
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    ops.alterTable(
+        tableIdent(schema, "iceberg_alter"),
+        TableChange.setProperty(
+            GlueConstants.METADATA_LOCATION, 
"s3://bucket/path/metadata/v2.metadata.json"));
+
+    Table loaded = ops.loadTable(tableIdent(schema, "iceberg_alter"));
+    assertEquals(
+        "s3://bucket/path/metadata/v2.metadata.json",
+        loaded.properties().get(GlueConstants.METADATA_LOCATION));
+  }
+
+  @Test
+  void testListTablesIncludesIceberg() {
+    String schema = newSchema();
+    ops.createSchema(schemaIdent(schema), null, Collections.emptyMap());
+
+    createHiveTable(schema, "hive_mixed");
+
+    Map<String, String> icebergProps = new HashMap<>();
+    icebergProps.put(GlueConstants.TABLE_FORMAT, "ICEBERG");
+    icebergProps.put(GlueConstants.METADATA_LOCATION, 
"s3://bucket/path/metadata/v1.metadata.json");
+    ops.createTable(
+        tableIdent(schema, "iceberg_mixed"),
+        new Column[0],
+        null,
+        icebergProps,
+        new Transform[0],
+        Distributions.NONE,
+        new SortOrder[0],
+        new Index[0]);
+
+    List<String> names =
+        Arrays.stream(ops.listTables(tableNs(schema)))
+            .map(NameIdentifier::name)
+            .collect(Collectors.toList());
+    assertTrue(names.contains("hive_mixed"));
+    assertTrue(names.contains("iceberg_mixed"));
+  }
+}
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AwsGlueCatalogIT.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AwsGlueCatalogIT.java
new file mode 100644
index 0000000000..7f8e1afa53
--- /dev/null
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/AwsGlueCatalogIT.java
@@ -0,0 +1,57 @@
+/*
+ * 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.catalog.glue.integration.test;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.gravitino.catalog.glue.GlueConstants;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+/**
+ * Runs {@link AbstractGlueCatalogIT} scenarios against a real AWS Glue 
endpoint.
+ *
+ * <p>This test is <b>skipped by default</b> and only runs when {@code 
AWS_ACCESS_KEY_ID} is set.
+ * Required environment variables:
+ *
+ * <ul>
+ *   <li>{@code AWS_ACCESS_KEY_ID}
+ *   <li>{@code AWS_SECRET_ACCESS_KEY}
+ *   <li>{@code AWS_DEFAULT_REGION} (e.g. {@code us-east-1})
+ *   <li>{@code GLUE_CATALOG_ID} (12-digit AWS account ID; optional)
+ * </ul>
+ */
+@EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = ".+")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class AwsGlueCatalogIT extends AbstractGlueCatalogIT {
+
+  @Override
+  protected Map<String, String> catalogConfig() {
+    Map<String, String> config = new HashMap<>();
+    config.put(
+        GlueConstants.AWS_REGION, 
System.getenv().getOrDefault("AWS_DEFAULT_REGION", "us-east-1"));
+    config.put(GlueConstants.AWS_ACCESS_KEY_ID, 
System.getenv("AWS_ACCESS_KEY_ID"));
+    config.put(GlueConstants.AWS_SECRET_ACCESS_KEY, 
System.getenv("AWS_SECRET_ACCESS_KEY"));
+    String catalogId = System.getenv("GLUE_CATALOG_ID");
+    if (catalogId != null) {
+      config.put(GlueConstants.AWS_GLUE_CATALOG_ID, catalogId);
+    }
+    return config;
+  }
+}
diff --git 
a/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/MotoGlueCatalogIT.java
 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/MotoGlueCatalogIT.java
new file mode 100644
index 0000000000..979affbcb2
--- /dev/null
+++ 
b/catalogs/catalog-glue/src/test/java/org/apache/gravitino/catalog/glue/integration/test/MotoGlueCatalogIT.java
@@ -0,0 +1,80 @@
+/*
+ * 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.catalog.glue.integration.test;
+
+import java.util.Map;
+import org.apache.gravitino.catalog.glue.GlueConstants;
+import org.apache.gravitino.integration.test.container.GravitinoMotoContainer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * Runs {@link AbstractGlueCatalogIT} scenarios against a Moto server (free 
AWS mock).
+ *
+ * <p>Requires Docker. Skipped by default when the {@code 
gravitino-docker-test} tag is excluded.
+ * Override the container image with the {@code 
GRAVITINO_CI_MOTO_DOCKER_IMAGE} environment
+ * variable.
+ */
+@Tag("gravitino-docker-test")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class MotoGlueCatalogIT extends AbstractGlueCatalogIT {
+
+  private static final String FALLBACK_IMAGE = "motoserver/moto:5.1.4";
+
+  private GravitinoMotoContainer moto;
+
+  @Override
+  @BeforeAll
+  void initOps() {
+    String image = GravitinoMotoContainer.DEFAULT_IMAGE;
+    if (image == null || image.isBlank()) {
+      image = FALLBACK_IMAGE;
+    }
+    moto = GravitinoMotoContainer.builder().withImage(image).build();
+    moto.start();
+    super.initOps();
+  }
+
+  @AfterAll
+  void stopContainer() {
+    if (moto != null) {
+      moto.close();
+    }
+  }
+
+  @Override
+  protected Map<String, String> catalogConfig() {
+    String endpoint =
+        "http://";
+            + moto.getContainer().getHost()
+            + ":"
+            + moto.getMappedPort(GravitinoMotoContainer.PORT);
+    return Map.of(
+        GlueConstants.AWS_REGION,
+        "us-east-1",
+        GlueConstants.AWS_ACCESS_KEY_ID,
+        "test",
+        GlueConstants.AWS_SECRET_ACCESS_KEY,
+        "test",
+        GlueConstants.AWS_GLUE_ENDPOINT,
+        endpoint);
+  }
+}
diff --git 
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/GravitinoMotoContainer.java
 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/GravitinoMotoContainer.java
new file mode 100644
index 0000000000..efaf29a524
--- /dev/null
+++ 
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/GravitinoMotoContainer.java
@@ -0,0 +1,74 @@
+/*
+ * 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.integration.test.container;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.testcontainers.containers.Network;
+
+/** Container wrapping {@code motoserver/moto} — a free AWS mock that supports 
Glue and more. */
+public class GravitinoMotoContainer extends BaseContainer {
+
+  public static final String DEFAULT_IMAGE = 
System.getenv("GRAVITINO_CI_MOTO_DOCKER_IMAGE");
+  public static final String HOST_NAME = "gravitino-ci-moto";
+  public static final int PORT = 5000;
+
+  public GravitinoMotoContainer(
+      String image,
+      String hostName,
+      Set<Integer> ports,
+      Map<String, String> extraHosts,
+      Map<String, String> filesToMount,
+      Map<String, String> envVars,
+      Optional<Network> network) {
+    super(image, hostName, ports, extraHosts, filesToMount, envVars, network);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  protected boolean checkContainerStatus(int retryLimit) {
+    return true;
+  }
+
+  public static class Builder
+      extends BaseContainer.Builder<GravitinoMotoContainer.Builder, 
GravitinoMotoContainer> {
+    public Builder() {
+      super();
+      this.image = DEFAULT_IMAGE;
+      this.hostName = HOST_NAME;
+      this.exposePorts = ImmutableSet.of(PORT);
+    }
+
+    @Override
+    public GravitinoMotoContainer build() {
+      if (image == null || image.isBlank()) {
+        throw new IllegalStateException(
+            "GravitinoMotoContainer image must be set. "
+                + "Provide a non-blank image via withImage() or set the 
GRAVITINO_CI_MOTO_DOCKER_IMAGE environment variable.");
+      }
+      return new GravitinoMotoContainer(
+          image, hostName, exposePorts, extraHosts, filesToMount, envVars, 
network);
+    }
+  }
+}

Reply via email to